Replace the control bar's stroke weight with a full stroke properties popover (#4145)
* Replace the control bar's stroke weight with a full stroke properties popover * Code review
This commit is contained in:
parent
629a1f4b4c
commit
4d5dce976e
|
|
@ -668,6 +668,7 @@ impl Diffable for WidgetInstance {
|
||||||
&& button1.tooltip_description == button2.tooltip_description
|
&& button1.tooltip_description == button2.tooltip_description
|
||||||
&& button1.tooltip_shortcut == button2.tooltip_shortcut
|
&& button1.tooltip_shortcut == button2.tooltip_shortcut
|
||||||
&& button1.popover_min_width == button2.popover_min_width
|
&& button1.popover_min_width == button2.popover_min_width
|
||||||
|
&& button1.popover_layout.0.len() == button2.popover_layout.0.len()
|
||||||
{
|
{
|
||||||
// Only the popover layout differs, diff it recursively
|
// Only the popover layout differs, diff it recursively
|
||||||
for (i, (a, b)) in button1.popover_layout.0.iter_mut().zip(button2.popover_layout.0.iter()).enumerate() {
|
for (i, (a, b)) in button1.popover_layout.0.iter_mut().zip(button2.popover_layout.0.iter()).enumerate() {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ pub struct TextLabel {
|
||||||
// Sizing
|
// Sizing
|
||||||
#[serde(rename = "minWidth")]
|
#[serde(rename = "minWidth")]
|
||||||
pub min_width: u32,
|
pub min_width: u32,
|
||||||
|
#[serde(rename = "maxWidth")]
|
||||||
|
pub max_width: u32,
|
||||||
#[serde(rename = "minWidthCharacters")]
|
#[serde(rename = "minWidthCharacters")]
|
||||||
pub min_width_characters: u32,
|
pub min_width_characters: u32,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
widgets.push(LayoutGroup::row(vec![TextLabel::new("Grid").bold(true).widget_instance()]));
|
widgets.push(LayoutGroup::row(vec![TextLabel::new("Grid").bold(true).widget_instance()]));
|
||||||
|
|
||||||
widgets.push(LayoutGroup::row(vec![
|
widgets.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Type").table_align(true).widget_instance(),
|
TextLabel::new("Type").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
RadioInput::new(vec![
|
RadioInput::new(vec![
|
||||||
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
|
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
|
||||||
|
|
@ -262,7 +262,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
]));
|
]));
|
||||||
|
|
||||||
let mut color_widgets = vec![
|
let mut color_widgets = vec![
|
||||||
TextLabel::new("Display").table_align(true).widget_instance(),
|
TextLabel::new("Display").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
];
|
];
|
||||||
color_widgets.extend([
|
color_widgets.extend([
|
||||||
|
|
@ -287,7 +287,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
widgets.push(LayoutGroup::row(color_widgets));
|
widgets.push(LayoutGroup::row(color_widgets));
|
||||||
|
|
||||||
widgets.push(LayoutGroup::row(vec![
|
widgets.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Origin").table_align(true).widget_instance(),
|
TextLabel::new("Origin").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(grid.origin.x))
|
NumberInput::new(Some(grid.origin.x))
|
||||||
.label("X")
|
.label("X")
|
||||||
|
|
@ -306,7 +306,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
|
|
||||||
match grid.grid_type {
|
match grid.grid_type {
|
||||||
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![
|
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Spacing").table_align(true).widget_instance(),
|
TextLabel::new("Spacing").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(spacing.x))
|
NumberInput::new(Some(spacing.x))
|
||||||
.label("X")
|
.label("X")
|
||||||
|
|
@ -326,7 +326,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
])),
|
])),
|
||||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
|
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
|
||||||
widgets.push(LayoutGroup::row(vec![
|
widgets.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Y Spacing").table_align(true).widget_instance(),
|
TextLabel::new("Y Spacing").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(y_axis_spacing))
|
NumberInput::new(Some(y_axis_spacing))
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
|
|
@ -336,7 +336,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
]));
|
]));
|
||||||
widgets.push(LayoutGroup::row(vec![
|
widgets.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("Angles").table_align(true).widget_instance(),
|
TextLabel::new("Angles").min_width(60).max_width(60).table_align(true).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(angle_a))
|
NumberInput::new(Some(angle_a))
|
||||||
.unit("°")
|
.unit("°")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::utility_types::DocumentToolData;
|
use crate::messages::tool::utility_types::DocumentToolData;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
|
||||||
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
||||||
pub struct ToolColorOptions {
|
pub struct ToolColorOptions {
|
||||||
|
|
@ -109,13 +109,25 @@ impl ToolColorOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared per-tool state for drawing tools that produce a stroked-and-filled shape (Shape, Pen, Freehand, Spline).
|
/// 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 {
|
pub struct DrawingToolState {
|
||||||
/// The current stroke weight. `None` = mixed across selected layers.
|
/// The current stroke weight. `None` = mixed across selected layers.
|
||||||
pub line_weight: Option<f64>,
|
pub line_weight: Option<f64>,
|
||||||
/// Persistent default weight, updated when the user edits the weight while no layer is selected.
|
/// Persistent default weight, updated when the user edits the weight while no layer is selected.
|
||||||
pub default_line_weight: f64,
|
pub default_line_weight: f64,
|
||||||
|
/// Stroke alignment from the selection. `None` = mixed.
|
||||||
|
pub stroke_align: Option<StrokeAlign>,
|
||||||
|
/// Stroke cap from the selection. `None` = mixed.
|
||||||
|
pub stroke_cap: Option<StrokeCap>,
|
||||||
|
/// Stroke join from the selection. `None` = mixed.
|
||||||
|
pub stroke_join: Option<StrokeJoin>,
|
||||||
|
/// Stroke miter limit from the selection. `None` = mixed.
|
||||||
|
pub miter_limit: Option<f64>,
|
||||||
|
/// Paint order from the selection. `None` = mixed.
|
||||||
|
pub paint_order: Option<PaintOrder>,
|
||||||
|
/// Dash lengths from the selection. `None` = mixed.
|
||||||
|
pub dash_lengths: Option<Vec<f64>>,
|
||||||
|
/// Dash offset from the selection. `None` = mixed.
|
||||||
|
pub dash_offset: Option<f64>,
|
||||||
/// Set of layers we last synced from, used to detect real selection changes vs. internal node toggles.
|
/// Set of layers we last synced from, used to detect real selection changes vs. internal node toggles.
|
||||||
pub last_synced_selection: Vec<LayerNodeIdentifier>,
|
pub last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||||
/// The fill swatch's color, checkbox, and mixed state.
|
/// The fill swatch's color, checkbox, and mixed state.
|
||||||
|
|
@ -132,6 +144,13 @@ impl DrawingToolState {
|
||||||
Self {
|
Self {
|
||||||
line_weight: Some(DEFAULT_STROKE_WIDTH),
|
line_weight: Some(DEFAULT_STROKE_WIDTH),
|
||||||
default_line_weight: DEFAULT_STROKE_WIDTH,
|
default_line_weight: DEFAULT_STROKE_WIDTH,
|
||||||
|
stroke_align: Some(StrokeAlign::default()),
|
||||||
|
stroke_cap: Some(StrokeCap::default()),
|
||||||
|
stroke_join: Some(StrokeJoin::default()),
|
||||||
|
miter_limit: Some(4.),
|
||||||
|
paint_order: Some(PaintOrder::default()),
|
||||||
|
dash_lengths: Some(Vec::new()),
|
||||||
|
dash_offset: Some(0.),
|
||||||
last_synced_selection: Vec::new(),
|
last_synced_selection: Vec::new(),
|
||||||
fill: if fill_enabled { ToolColorOptions::new_enabled() } else { ToolColorOptions::new_disabled() },
|
fill: if fill_enabled { ToolColorOptions::new_enabled() } else { ToolColorOptions::new_disabled() },
|
||||||
stroke: ToolColorOptions::new_enabled(),
|
stroke: ToolColorOptions::new_enabled(),
|
||||||
|
|
@ -143,6 +162,33 @@ impl DrawingToolState {
|
||||||
pub fn effective_line_weight(&self) -> f64 {
|
pub fn effective_line_weight(&self) -> f64 {
|
||||||
self.line_weight.unwrap_or(self.default_line_weight)
|
self.line_weight.unwrap_or(self.default_line_weight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dash lengths to apply, falling back to empty when [`Self::dash_lengths`] is `None` (mixed).
|
||||||
|
pub fn effective_dash_lengths(&self) -> Vec<f64> {
|
||||||
|
self.dash_lengths.clone().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a stroke to a freshly created `layer` using the tool's currently selected color, weight, and stroke options (align, cap, join, etc.).
|
||||||
|
/// Used by the drawing tools at shape-creation time so new shapes inherit the popover's options instead of defaulting to the `Stroke` struct's defaults.
|
||||||
|
pub fn apply_stroke_to_new_layer(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||||
|
if !self.stroke.is_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(FillChoice::Solid(color)) = &self.stroke.fill_choice else { return };
|
||||||
|
let stroke = graphene_std::vector::style::Stroke {
|
||||||
|
color: Some(*color),
|
||||||
|
weight: self.effective_line_weight(),
|
||||||
|
align: self.stroke_align.unwrap_or_default(),
|
||||||
|
cap: self.stroke_cap.unwrap_or_default(),
|
||||||
|
join: self.stroke_join.unwrap_or_default(),
|
||||||
|
join_miter_limit: self.miter_limit.unwrap_or(4.),
|
||||||
|
paint_order: self.paint_order.unwrap_or_default(),
|
||||||
|
dash_lengths: self.effective_dash_lengths(),
|
||||||
|
dash_offset: self.dash_offset.unwrap_or(0.),
|
||||||
|
transform: glam::DAffine2::IDENTITY,
|
||||||
|
};
|
||||||
|
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `FillChoice::Solid` from a linear-space color, applying gamma conversion to display sRGB.
|
/// Builds a `FillChoice::Solid` from a linear-space color, applying gamma conversion to display sRGB.
|
||||||
|
|
@ -250,9 +296,72 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
|
||||||
needs_refresh = true;
|
needs_refresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
needs_refresh |= sync_stroke_options(drawing, document);
|
||||||
|
|
||||||
needs_refresh
|
needs_refresh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
|
||||||
|
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
|
||||||
|
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
|
||||||
|
let strokes: Vec<_> = document
|
||||||
|
.network_interface
|
||||||
|
.selected_nodes()
|
||||||
|
.selected_layers_except_artboards(&document.network_interface)
|
||||||
|
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
|
||||||
|
.collect();
|
||||||
|
if strokes.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unanimous<T: PartialEq + Clone>(values: impl IntoIterator<Item = T>) -> Option<T> {
|
||||||
|
let mut iter = values.into_iter();
|
||||||
|
let first = iter.next()?;
|
||||||
|
iter.all(|v| v == first).then_some(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_align = unanimous(strokes.iter().map(|s| s.align));
|
||||||
|
let new_cap = unanimous(strokes.iter().map(|s| s.cap));
|
||||||
|
let new_join = unanimous(strokes.iter().map(|s| s.join));
|
||||||
|
let new_miter = unanimous(strokes.iter().map(|s| s.miter_limit));
|
||||||
|
let new_paint_order = unanimous(strokes.iter().map(|s| s.paint_order));
|
||||||
|
let new_dash_lengths = unanimous(strokes.iter().map(|s| &s.dash_lengths)).cloned();
|
||||||
|
let new_dash_offset = unanimous(strokes.iter().map(|s| s.dash_offset));
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
if drawing.stroke_align != new_align {
|
||||||
|
drawing.stroke_align = new_align;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.stroke_cap != new_cap {
|
||||||
|
drawing.stroke_cap = new_cap;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.stroke_join != new_join {
|
||||||
|
drawing.stroke_join = new_join;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.miter_limit != new_miter {
|
||||||
|
drawing.miter_limit = new_miter;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.paint_order != new_paint_order {
|
||||||
|
drawing.paint_order = new_paint_order;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.dash_lengths != new_dash_lengths {
|
||||||
|
drawing.dash_lengths = new_dash_lengths;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if drawing.dash_offset != new_dash_offset {
|
||||||
|
drawing.dash_offset = new_dash_offset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
/// 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 {
|
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 fill_fallback = solid_gamma(fill_color);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster};
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::misc::ManipulatorPointId;
|
use graphene_std::vector::misc::ManipulatorPointId;
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, Gradient};
|
use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
|
@ -493,6 +493,66 @@ pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subset of Stroke node inputs read for the control bar's stroke options popover.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct StrokeOptionsState {
|
||||||
|
pub align: StrokeAlign,
|
||||||
|
pub cap: StrokeCap,
|
||||||
|
pub join: StrokeJoin,
|
||||||
|
pub miter_limit: f64,
|
||||||
|
pub paint_order: PaintOrder,
|
||||||
|
pub dash_lengths: Vec<f64>,
|
||||||
|
pub dash_offset: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the non-color stroke option inputs from a layer's Stroke proto node. Returns `None` when the layer has no Stroke node.
|
||||||
|
/// Inputs that aren't a static value (e.g. wired to another node) fall back to per-field defaults so the layer still participates in the sync.
|
||||||
|
pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<StrokeOptionsState> {
|
||||||
|
let stroke = &DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let layer_view = NodeGraphLayer::new(layer, network_interface);
|
||||||
|
layer_view.upstream_node_id_from_name(stroke)?;
|
||||||
|
let read = |index: usize| layer_view.find_input(stroke, index);
|
||||||
|
|
||||||
|
let align = match read(graphene_std::vector::stroke::AlignInput::INDEX) {
|
||||||
|
Some(TaggedValue::StrokeAlign(value)) => *value,
|
||||||
|
_ => StrokeAlign::default(),
|
||||||
|
};
|
||||||
|
let cap = match read(graphene_std::vector::stroke::CapInput::INDEX) {
|
||||||
|
Some(TaggedValue::StrokeCap(value)) => *value,
|
||||||
|
_ => StrokeCap::default(),
|
||||||
|
};
|
||||||
|
let join = match read(graphene_std::vector::stroke::JoinInput::INDEX) {
|
||||||
|
Some(TaggedValue::StrokeJoin(value)) => *value,
|
||||||
|
_ => StrokeJoin::default(),
|
||||||
|
};
|
||||||
|
let miter_limit = match read(graphene_std::vector::stroke::MiterLimitInput::INDEX) {
|
||||||
|
Some(TaggedValue::F64(value)) => *value,
|
||||||
|
_ => 4.,
|
||||||
|
};
|
||||||
|
let paint_order = match read(graphene_std::vector::stroke::PaintOrderInput::INDEX) {
|
||||||
|
Some(TaggedValue::PaintOrder(value)) => *value,
|
||||||
|
_ => PaintOrder::default(),
|
||||||
|
};
|
||||||
|
let dash_lengths = match read(graphene_std::vector::stroke::DashLengthsInput::<List<f64>>::INDEX) {
|
||||||
|
Some(TaggedValue::F64Array(value)) => value.clone(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
|
let dash_offset = match read(graphene_std::vector::stroke::DashOffsetInput::INDEX) {
|
||||||
|
Some(TaggedValue::F64(value)) => *value,
|
||||||
|
_ => 0.,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(StrokeOptionsState {
|
||||||
|
align,
|
||||||
|
cap,
|
||||||
|
join,
|
||||||
|
miter_limit,
|
||||||
|
paint_order,
|
||||||
|
dash_lengths,
|
||||||
|
dash_offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the node ID of a layer's upstream Stroke proto node, if one exists.
|
/// Returns the node ID of a layer's upstream Stroke proto node, if one exists.
|
||||||
pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
||||||
|
|
@ -759,9 +819,10 @@ impl<'a> NodeGraphLayer<'a> {
|
||||||
self.network_interface.upstream_flow_back_from_nodes(vec![self.layer_node], &[], FlowType::HorizontalFlow)
|
self.network_interface.upstream_flow_back_from_nodes(vec![self.layer_node], &[], FlowType::HorizontalFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Node id of a node if it exists in the layer's primary flow
|
/// Node id of a node if it exists in this specific layer's primary flow, stopping at the next layer upstream so a group doesn't incorrectly match its children's nodes.
|
||||||
pub fn upstream_node_id_from_name(&self, identifier: &DefinitionIdentifier) -> Option<NodeId> {
|
pub fn upstream_node_id_from_name(&self, identifier: &DefinitionIdentifier) -> Option<NodeId> {
|
||||||
self.horizontal_layer_flow()
|
self.horizontal_layer_flow()
|
||||||
|
.take_while(|&node_id| node_id == self.layer_node || !self.network_interface.is_layer(&node_id, &[]))
|
||||||
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == *identifier))
|
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == *identifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,6 @@ pub mod resize;
|
||||||
pub mod shape_editor;
|
pub mod shape_editor;
|
||||||
pub mod shapes;
|
pub mod shapes;
|
||||||
pub mod snapping;
|
pub mod snapping;
|
||||||
|
pub mod stroke_options;
|
||||||
pub mod transformation_cage;
|
pub mod transformation_cage;
|
||||||
pub mod utility_functions;
|
pub mod utility_functions;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
use crate::messages::prelude::*;
|
||||||
|
use crate::messages::tool::common_functionality::color_selector::{DrawingToolState, apply_line_weight};
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::NodeInputDecleration;
|
||||||
|
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||||
|
use graphene_std::list::List;
|
||||||
|
use graphene_std::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
|
||||||
|
/// All non-color stroke-related options surfaced in the control bar popover.
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum StrokeOptionsUpdate {
|
||||||
|
LineWeight(f64),
|
||||||
|
Align(StrokeAlign),
|
||||||
|
Cap(StrokeCap),
|
||||||
|
Join(StrokeJoin),
|
||||||
|
MiterLimit(f64),
|
||||||
|
PaintOrder(PaintOrder),
|
||||||
|
DashLengths(Vec<f64>),
|
||||||
|
DashOffset(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the control-bar popover button that opens the stroke options panel (weight, align, caps, joins, miter limit, paint order, dash).
|
||||||
|
/// `to_message` adapts a [`StrokeOptionsUpdate`] into the calling tool's `UpdateOptions` message.
|
||||||
|
pub fn create_stroke_options_popover_widget<F>(drawing: &DrawingToolState, disabled: bool, to_message: F) -> WidgetInstance
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
PopoverButton::new()
|
||||||
|
.popover_layout(Layout(build_popover_rows(drawing, to_message)))
|
||||||
|
.disabled(disabled)
|
||||||
|
.tooltip_label("Stroke Options")
|
||||||
|
.widget_instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches a [`StrokeOptionsUpdate`] to the matching apply helper and updates `drawing` in lockstep.
|
||||||
|
pub fn apply_stroke_option(drawing: &mut DrawingToolState, update: StrokeOptionsUpdate, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
match update {
|
||||||
|
StrokeOptionsUpdate::LineWeight(weight) => apply_line_weight(drawing, weight, document, responses),
|
||||||
|
StrokeOptionsUpdate::Align(align) => apply_stroke_align(drawing, align, document, responses),
|
||||||
|
StrokeOptionsUpdate::Cap(cap) => apply_stroke_cap(drawing, cap, document, responses),
|
||||||
|
StrokeOptionsUpdate::Join(join) => apply_stroke_join(drawing, join, document, responses),
|
||||||
|
StrokeOptionsUpdate::MiterLimit(limit) => apply_miter_limit(drawing, limit, document, responses),
|
||||||
|
StrokeOptionsUpdate::PaintOrder(order) => apply_paint_order(drawing, order, document, responses),
|
||||||
|
StrokeOptionsUpdate::DashLengths(lengths) => apply_dash_lengths(drawing, lengths, document, responses),
|
||||||
|
StrokeOptionsUpdate::DashOffset(offset) => apply_dash_offset(drawing, offset, document, responses),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_popover_rows<F>(drawing: &DrawingToolState, to_message: F) -> Vec<LayoutGroup>
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
// Miter limit only matters when the join is `Miter`; mixed (`None`) keeps the row visible so the user can still edit the value.
|
||||||
|
let show_miter_limit = drawing.stroke_join != Some(StrokeJoin::Bevel) && drawing.stroke_join != Some(StrokeJoin::Round);
|
||||||
|
// Mixed dash patterns (`None`) keep the offset row visible so the user can still edit the offset when at least some selected layers have dashes.
|
||||||
|
let has_dash = drawing.dash_lengths.as_deref().is_none_or(|lengths| !lengths.is_empty());
|
||||||
|
|
||||||
|
let mut rows = vec![
|
||||||
|
LayoutGroup::row(vec![TextLabel::new("Stroke").bold(true).widget_instance()]),
|
||||||
|
LayoutGroup::row(weight_row(drawing.line_weight, to_message.clone())),
|
||||||
|
LayoutGroup::row(dash_lengths_row(drawing.dash_lengths.as_deref(), to_message.clone())),
|
||||||
|
];
|
||||||
|
if has_dash {
|
||||||
|
rows.push(LayoutGroup::row(dash_offset_row(drawing.dash_offset, to_message.clone())));
|
||||||
|
}
|
||||||
|
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", drawing.paint_order, false, {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
move |value| to_message(StrokeOptionsUpdate::PaintOrder(value))
|
||||||
|
})));
|
||||||
|
rows.push(LayoutGroup::row(enum_radio_row::<StrokeAlign, _>("Align", drawing.stroke_align, false, {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
move |value| to_message(StrokeOptionsUpdate::Align(value))
|
||||||
|
})));
|
||||||
|
rows.push(LayoutGroup::row(enum_radio_row::<StrokeCap, _>("Cap", drawing.stroke_cap, false, {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
move |value| to_message(StrokeOptionsUpdate::Cap(value))
|
||||||
|
})));
|
||||||
|
rows.push(LayoutGroup::row(enum_radio_row::<StrokeJoin, _>("Join", drawing.stroke_join, false, {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
move |value| to_message(StrokeOptionsUpdate::Join(value))
|
||||||
|
})));
|
||||||
|
if show_miter_limit {
|
||||||
|
rows.push(LayoutGroup::row(miter_limit_row(drawing.miter_limit, to_message)));
|
||||||
|
}
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weight_row<F>(weight: Option<f64>, to_message: F) -> Vec<WidgetInstance>
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
vec![
|
||||||
|
TextLabel::new("Weight").table_align(true).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
NumberInput::new(weight)
|
||||||
|
.unit(" px")
|
||||||
|
.min(0.)
|
||||||
|
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||||
|
.on_update(move |number: &NumberInput| number.value.map_or(Message::NoOp, |value| to_message(StrokeOptionsUpdate::LineWeight(value))))
|
||||||
|
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||||
|
.widget_instance(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn miter_limit_row<F>(limit: Option<f64>, to_message: F) -> Vec<WidgetInstance>
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
vec![
|
||||||
|
TextLabel::new("Limit").table_align(true).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
NumberInput::new(limit)
|
||||||
|
.min(0.)
|
||||||
|
.on_update(move |number: &NumberInput| number.value.map_or(Message::NoOp, |value| to_message(StrokeOptionsUpdate::MiterLimit(value))))
|
||||||
|
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||||
|
.widget_instance(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enum_radio_row<E, F>(label_text: &str, current: Option<E>, disabled: bool, to_message: F) -> Vec<WidgetInstance>
|
||||||
|
where
|
||||||
|
E: ChoiceTypeStatic + 'static,
|
||||||
|
F: Fn(E) -> Message + 'static + Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
let entries = E::list()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|section| section.iter())
|
||||||
|
.map(|(value, meta)| {
|
||||||
|
let to_message = to_message.clone();
|
||||||
|
let value = *value;
|
||||||
|
let entry = RadioEntryData::new(meta.name)
|
||||||
|
.tooltip_label(meta.label)
|
||||||
|
.tooltip_description(meta.description.unwrap_or_default())
|
||||||
|
.on_update(move |_| to_message(value))
|
||||||
|
.on_commit(|_| DocumentMessage::StartTransaction.into());
|
||||||
|
if let Some(icon) = meta.icon { entry.icon(icon) } else { entry.label(meta.label) }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
vec![
|
||||||
|
TextLabel::new(label_text).table_align(true).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
RadioInput::new(entries).selected_index(current.map(|c| c.as_u32())).disabled(disabled).widget_instance(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dash_lengths_row<F>(current: Option<&[f64]>, to_message: F) -> Vec<WidgetInstance>
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
let text = current
|
||||||
|
.map(|values| values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
vec![
|
||||||
|
TextLabel::new("Dash").table_align(true).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
TextInput::new(text)
|
||||||
|
.centered(true)
|
||||||
|
.tooltip_label("Dash Pattern")
|
||||||
|
.tooltip_description("Comma-separated dash and gap lengths.")
|
||||||
|
.on_update(move |input: &TextInput| {
|
||||||
|
let parsed = input.value.split(&[',', ' ']).filter(|piece| !piece.is_empty()).map(str::parse::<f64>).collect::<Result<Vec<_>, _>>();
|
||||||
|
parsed.map_or(Message::NoOp, |lengths| to_message(StrokeOptionsUpdate::DashLengths(lengths)))
|
||||||
|
})
|
||||||
|
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||||
|
.widget_instance(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dash_offset_row<F>(offset: Option<f64>, to_message: F) -> Vec<WidgetInstance>
|
||||||
|
where
|
||||||
|
F: Fn(StrokeOptionsUpdate) -> Message + 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
vec![
|
||||||
|
TextLabel::new("Offset").table_align(true).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
NumberInput::new(offset)
|
||||||
|
.unit(" px")
|
||||||
|
.on_update(move |number: &NumberInput| number.value.map_or(Message::NoOp, |value| to_message(StrokeOptionsUpdate::DashOffset(value))))
|
||||||
|
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
||||||
|
.widget_instance(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============
|
||||||
|
// APPLY HELPERS
|
||||||
|
// =============
|
||||||
|
|
||||||
|
pub fn apply_stroke_align(drawing: &mut DrawingToolState, align: StrokeAlign, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.stroke_align = Some(align);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::AlignInput::INDEX, TaggedValue::StrokeAlign(align), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_stroke_cap(drawing: &mut DrawingToolState, cap: StrokeCap, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.stroke_cap = Some(cap);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::CapInput::INDEX, TaggedValue::StrokeCap(cap), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_stroke_join(drawing: &mut DrawingToolState, join: StrokeJoin, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.stroke_join = Some(join);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::JoinInput::INDEX, TaggedValue::StrokeJoin(join), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.miter_limit = Some(limit);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::MiterLimitInput::INDEX, TaggedValue::F64(limit), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.paint_order = Some(order);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::PaintOrderInput::INDEX, TaggedValue::PaintOrder(order), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.dash_lengths = Some(lengths.clone());
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashLengthsInput::<List<f64>>::INDEX, TaggedValue::F64Array(lengths), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_dash_offset(drawing: &mut DrawingToolState, offset: f64, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
drawing.dash_offset = Some(offset);
|
||||||
|
set_stroke_input_for_selected(document, graphene_std::vector::stroke::DashOffsetInput::INDEX, TaggedValue::F64(offset), responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_stroke_input_for_selected(document: &DocumentMessageHandler, input_index: usize, value: TaggedValue, responses: &mut VecDeque<Message>) {
|
||||||
|
graph_modification_utils::set_proto_node_input_for_selected_layers(document, graphene_std::vector::stroke::IDENTIFIER, input_index, value, responses);
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,11 @@ use crate::messages::portfolio::document::overlays::utility_functions::path_endp
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
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,
|
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation, swap_fill_and_stroke,
|
||||||
swap_fill_and_stroke, sync_drawing_state,
|
sync_drawing_state,
|
||||||
};
|
};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||||
use crate::messages::tool::common_functionality::utility_functions::should_extend;
|
use crate::messages::tool::common_functionality::utility_functions::should_extend;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
|
@ -58,7 +59,7 @@ pub enum FreehandToolMessage {
|
||||||
pub enum FreehandOptionsUpdate {
|
pub enum FreehandOptionsUpdate {
|
||||||
FillColor(FillChoice),
|
FillColor(FillChoice),
|
||||||
FillEnabled(bool),
|
FillEnabled(bool),
|
||||||
LineWeight(f64),
|
StrokeOption(StrokeOptionsUpdate),
|
||||||
StrokeColor(Option<Color>),
|
StrokeColor(Option<Color>),
|
||||||
StrokeEnabled(bool),
|
StrokeEnabled(bool),
|
||||||
SwapFillAndStroke,
|
SwapFillAndStroke,
|
||||||
|
|
@ -84,29 +85,6 @@ impl ToolMetadata for FreehandTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
|
||||||
if let Some(value) = number_input.value {
|
|
||||||
FreehandToolMessage::UpdateOptions {
|
|
||||||
options: FreehandOptionsUpdate::LineWeight(value),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Message::NoOp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
|
||||||
.widget_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutHolder for FreehandTool {
|
impl LayoutHolder for FreehandTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||||
|
|
@ -154,9 +132,13 @@ impl LayoutHolder for FreehandTool {
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
|
||||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
widgets.push(create_stroke_options_popover_widget(&self.options.drawing, weight_disabled, |update| {
|
||||||
|
FreehandToolMessage::UpdateOptions {
|
||||||
|
options: FreehandOptionsUpdate::StrokeOption(update),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
|
||||||
Layout(vec![LayoutGroup::row(widgets)])
|
Layout(vec![LayoutGroup::row(widgets)])
|
||||||
}
|
}
|
||||||
|
|
@ -193,8 +175,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
|
||||||
FreehandOptionsUpdate::FillEnabled(enabled) => {
|
FreehandOptionsUpdate::FillEnabled(enabled) => {
|
||||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||||
}
|
}
|
||||||
FreehandOptionsUpdate::LineWeight(line_weight) => {
|
FreehandOptionsUpdate::StrokeOption(update) => {
|
||||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||||
}
|
}
|
||||||
FreehandOptionsUpdate::StrokeColor(color) => {
|
FreehandOptionsUpdate::StrokeColor(color) => {
|
||||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||||
|
|
@ -244,7 +226,6 @@ impl ToolTransition for FreehandTool {
|
||||||
struct FreehandToolData {
|
struct FreehandToolData {
|
||||||
end_point: Option<(DVec2, PointId)>,
|
end_point: Option<(DVec2, PointId)>,
|
||||||
dragged: bool,
|
dragged: bool,
|
||||||
weight: f64,
|
|
||||||
layer: Option<LayerNodeIdentifier>,
|
layer: Option<LayerNodeIdentifier>,
|
||||||
/// Viewport-space start position for newly created layers, used to compute local-space
|
/// Viewport-space start position for newly created layers, used to compute local-space
|
||||||
/// positions before the deferred TransformSet has been reflected in metadata.
|
/// positions before the deferred TransformSet has been reflected in metadata.
|
||||||
|
|
@ -283,7 +264,6 @@ impl Fsm for FreehandToolFsmState {
|
||||||
|
|
||||||
tool_data.dragged = false;
|
tool_data.dragged = false;
|
||||||
tool_data.end_point = None;
|
tool_data.end_point = None;
|
||||||
tool_data.weight = tool_options.drawing.effective_line_weight();
|
|
||||||
tool_data.new_layer_viewport_start = None;
|
tool_data.new_layer_viewport_start = None;
|
||||||
|
|
||||||
// Extend an endpoint of the selected path
|
// Extend an endpoint of the selected path
|
||||||
|
|
@ -322,7 +302,7 @@ impl Fsm for FreehandToolFsmState {
|
||||||
let nodes = vec![(NodeId(0), node)];
|
let nodes = vec![(NodeId(0), node)];
|
||||||
|
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_data.weight, layer, responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
tool_data.layer = Some(layer);
|
tool_data.layer = Some(layer);
|
||||||
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
||||||
|
|
@ -452,6 +432,7 @@ mod test_freehand {
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
|
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_stroke_width};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_stroke_width};
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::StrokeOptionsUpdate;
|
||||||
use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate;
|
use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate;
|
||||||
use crate::test_utils::test_prelude::*;
|
use crate::test_utils::test_prelude::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
@ -776,7 +757,7 @@ mod test_freehand {
|
||||||
let custom_line_weight = 5.;
|
let custom_line_weight = 5.;
|
||||||
editor
|
editor
|
||||||
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions {
|
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions {
|
||||||
options: FreehandOptionsUpdate::LineWeight(custom_line_weight),
|
options: FreehandOptionsUpdate::StrokeOption(StrokeOptionsUpdate::LineWeight(custom_line_weight)),
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@ use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles,
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
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::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
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,
|
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation, swap_fill_and_stroke,
|
||||||
swap_fill_and_stroke, sync_drawing_state,
|
sync_drawing_state,
|
||||||
};
|
};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers};
|
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::shape_editor::ShapeState;
|
||||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
|
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
|
|
@ -121,7 +122,7 @@ pub enum PenOverlayMode {
|
||||||
pub enum PenOptionsUpdate {
|
pub enum PenOptionsUpdate {
|
||||||
FillColor(FillChoice),
|
FillColor(FillChoice),
|
||||||
FillEnabled(bool),
|
FillEnabled(bool),
|
||||||
LineWeight(f64),
|
StrokeOption(StrokeOptionsUpdate),
|
||||||
StrokeColor(Option<Color>),
|
StrokeColor(Option<Color>),
|
||||||
StrokeEnabled(bool),
|
StrokeEnabled(bool),
|
||||||
SwapFillAndStroke,
|
SwapFillAndStroke,
|
||||||
|
|
@ -141,29 +142,6 @@ impl ToolMetadata for PenTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
|
||||||
if let Some(value) = number_input.value {
|
|
||||||
PenToolMessage::UpdateOptions {
|
|
||||||
options: PenOptionsUpdate::LineWeight(value),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Message::NoOp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
|
||||||
.widget_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutHolder for PenTool {
|
impl LayoutHolder for PenTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||||
|
|
@ -212,10 +190,13 @@ impl LayoutHolder for PenTool {
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
|
||||||
|
|
||||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
widgets.push(create_stroke_options_popover_widget(&self.options.drawing, weight_disabled, |update| {
|
||||||
|
PenToolMessage::UpdateOptions {
|
||||||
|
options: PenOptionsUpdate::StrokeOption(update),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorStyle::Section).widget_instance());
|
widgets.push(Separator::new(SeparatorStyle::Section).widget_instance());
|
||||||
|
|
||||||
|
|
@ -277,8 +258,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenT
|
||||||
self.options.pen_overlay_mode = overlay_mode_type;
|
self.options.pen_overlay_mode = overlay_mode_type;
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
}
|
}
|
||||||
PenOptionsUpdate::LineWeight(line_weight) => {
|
PenOptionsUpdate::StrokeOption(update) => {
|
||||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||||
}
|
}
|
||||||
PenOptionsUpdate::FillColor(fill_choice) => {
|
PenOptionsUpdate::FillColor(fill_choice) => {
|
||||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||||
|
|
@ -1316,7 +1297,7 @@ impl PenToolData {
|
||||||
let parent = document.new_layer_bounding_artboard(input, viewport);
|
let parent = document.new_layer_bounding_artboard(input, viewport);
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
self.current_layer = Some(layer);
|
self.current_layer = Some(layer);
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
self.prior_segment = None;
|
self.prior_segment = None;
|
||||||
self.prior_segments = None;
|
self.prior_segments = None;
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,44 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
|
||||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||||
use crate::messages::preferences::SelectionMode;
|
use crate::messages::preferences::SelectionMode;
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
|
use crate::messages::tool::common_functionality::color_selector::{
|
||||||
|
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, swap_fill_and_stroke, sync_drawing_state,
|
||||||
|
};
|
||||||
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::measure;
|
use crate::messages::tool::common_functionality::measure;
|
||||||
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
||||||
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
||||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||||
use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
|
use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
|
||||||
use glam::DMat2;
|
use glam::DMat2;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::Color;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::renderer::Rect;
|
use graphene_std::renderer::Rect;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
|
use graphene_std::vector::style::FillChoice;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct SelectTool {
|
pub struct SelectTool {
|
||||||
fsm_state: SelectToolFsmState,
|
fsm_state: SelectToolFsmState,
|
||||||
tool_data: SelectToolData,
|
tool_data: SelectToolData,
|
||||||
|
drawing: DrawingToolState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SelectTool {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
fsm_state: SelectToolFsmState::default(),
|
||||||
|
tool_data: SelectToolData::default(),
|
||||||
|
drawing: DrawingToolState::new(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -41,12 +58,19 @@ pub struct SelectOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum SelectOptionsUpdate {
|
pub enum SelectOptionsUpdate {
|
||||||
NestedSelectionBehavior(NestedSelectionBehavior),
|
NestedSelectionBehavior(NestedSelectionBehavior),
|
||||||
PivotGizmoType(PivotGizmoType),
|
PivotGizmoType(PivotGizmoType),
|
||||||
SetPivotGizmoEnabled(bool),
|
SetPivotGizmoEnabled(bool),
|
||||||
TogglePivotPinned,
|
TogglePivotPinned,
|
||||||
|
FillColor(FillChoice),
|
||||||
|
FillEnabled(bool),
|
||||||
|
StrokeColor(Option<Color>),
|
||||||
|
StrokeEnabled(bool),
|
||||||
|
SwapFillAndStroke,
|
||||||
|
StrokeOption(StrokeOptionsUpdate),
|
||||||
|
WorkingColorsChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
|
@ -81,6 +105,8 @@ pub struct SelectToolPointerKeys {
|
||||||
pub enum SelectToolMessage {
|
pub enum SelectToolMessage {
|
||||||
// Standard messages
|
// Standard messages
|
||||||
Abort,
|
Abort,
|
||||||
|
SelectionChanged,
|
||||||
|
WorkingColorChanged,
|
||||||
Overlays {
|
Overlays {
|
||||||
context: OverlayContext,
|
context: OverlayContext,
|
||||||
},
|
},
|
||||||
|
|
@ -190,18 +216,6 @@ impl SelectTool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn turn_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetInstance> + use<> {
|
|
||||||
[(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")]
|
|
||||||
.into_iter()
|
|
||||||
.map(move |(degrees, icon, label)| {
|
|
||||||
IconButton::new(icon, 24)
|
|
||||||
.tooltip_label(label)
|
|
||||||
.on_update(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into())
|
|
||||||
.disabled(disabled)
|
|
||||||
.widget_instance()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetInstance> + use<> {
|
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetInstance> + use<> {
|
||||||
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
|
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
|
||||||
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
|
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
|
||||||
|
|
@ -222,6 +236,65 @@ impl LayoutHolder for SelectTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let mut widgets = Vec::new();
|
let mut widgets = Vec::new();
|
||||||
|
|
||||||
|
// Fill/Stroke widget set (only shown when there's a selection to apply edits to)
|
||||||
|
if self.tool_data.selected_layers_count > 0 {
|
||||||
|
widgets.append(&mut self.drawing.fill.create_widgets(
|
||||||
|
"Fill:",
|
||||||
|
|checkbox: &CheckboxInput| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::FillEnabled(checkbox.checked),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
},
|
||||||
|
|color: &ColorInput| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::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(|_| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::SwapFillAndStroke,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_instance(),
|
||||||
|
);
|
||||||
|
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
|
|
||||||
|
widgets.append(&mut self.drawing.stroke.create_widgets(
|
||||||
|
"Stroke:",
|
||||||
|
|checkbox: &CheckboxInput| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::StrokeEnabled(checkbox.checked),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
},
|
||||||
|
|color: &ColorInput| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::StrokeColor(color.value.as_solid()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
let weight_disabled = self.drawing.stroke.enabled == Some(false);
|
||||||
|
widgets.push(create_stroke_options_popover_widget(&self.drawing, weight_disabled, |update| {
|
||||||
|
SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::StrokeOption(update),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
|
||||||
|
widgets.push(Separator::new(SeparatorStyle::Section).widget_instance());
|
||||||
|
}
|
||||||
|
|
||||||
// Select mode (Deep/Shallow)
|
// Select mode (Deep/Shallow)
|
||||||
widgets.push(self.deep_selection_widget());
|
widgets.push(self.deep_selection_widget());
|
||||||
|
|
||||||
|
|
@ -259,10 +332,6 @@ impl LayoutHolder for SelectTool {
|
||||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets.extend(self.flip_widgets(disabled));
|
widgets.extend(self.flip_widgets(disabled));
|
||||||
|
|
||||||
// Turn
|
|
||||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
|
||||||
widgets.extend(self.turn_widgets(disabled));
|
|
||||||
|
|
||||||
// Boolean
|
// Boolean
|
||||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets.extend(self.boolean_widgets(self.tool_data.selected_layers_count));
|
widgets.extend(self.boolean_widgets(self.tool_data.selected_layers_count));
|
||||||
|
|
@ -275,16 +344,27 @@ impl LayoutHolder for SelectTool {
|
||||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SelectTool {
|
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SelectTool {
|
||||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||||
let mut redraw_reference_pivot = false;
|
let mut redraw_reference_pivot = false;
|
||||||
|
let mut drawing_options_changed = false;
|
||||||
|
|
||||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: ref option_update }) = message {
|
if matches!(&message, ToolMessage::Select(SelectToolMessage::SelectionChanged)) && sync_drawing_state(&mut self.drawing, true, true, context.global_tool_data, context.document) {
|
||||||
match *option_update {
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(&message, ToolMessage::Select(SelectToolMessage::WorkingColorChanged)) {
|
||||||
|
responses.add(SelectToolMessage::SelectOptions {
|
||||||
|
options: SelectOptionsUpdate::WorkingColorsChanged,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: option_update }) = &message {
|
||||||
|
match option_update {
|
||||||
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
|
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
|
||||||
self.tool_data.nested_selection_behavior = nested_selection_behavior;
|
self.tool_data.nested_selection_behavior = *nested_selection_behavior;
|
||||||
responses.add(ToolMessage::UpdateHints);
|
responses.add(ToolMessage::UpdateHints);
|
||||||
}
|
}
|
||||||
SelectOptionsUpdate::PivotGizmoType(gizmo_type) => {
|
SelectOptionsUpdate::PivotGizmoType(gizmo_type) => {
|
||||||
if self.tool_data.pivot_gizmo.state.enabled {
|
if self.tool_data.pivot_gizmo.state.enabled {
|
||||||
self.tool_data.pivot_gizmo.state.gizmo_type = gizmo_type;
|
self.tool_data.pivot_gizmo.state.gizmo_type = *gizmo_type;
|
||||||
responses.add(ToolMessage::UpdateHints);
|
responses.add(ToolMessage::UpdateHints);
|
||||||
let pivot_gizmo = self.tool_data.pivot_gizmo();
|
let pivot_gizmo = self.tool_data.pivot_gizmo();
|
||||||
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
||||||
|
|
@ -293,24 +373,51 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectOptionsUpdate::SetPivotGizmoEnabled(enabled) => {
|
SelectOptionsUpdate::SetPivotGizmoEnabled(enabled) => {
|
||||||
self.tool_data.pivot_gizmo.state.enabled = enabled;
|
self.tool_data.pivot_gizmo.state.enabled = *enabled;
|
||||||
responses.add(ToolMessage::UpdateHints);
|
responses.add(ToolMessage::UpdateHints);
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
redraw_reference_pivot = true;
|
redraw_reference_pivot = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectOptionsUpdate::TogglePivotPinned => {
|
SelectOptionsUpdate::TogglePivotPinned => {
|
||||||
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned;
|
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned;
|
||||||
responses.add(ToolMessage::UpdateHints);
|
responses.add(ToolMessage::UpdateHints);
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
redraw_reference_pivot = true;
|
redraw_reference_pivot = true;
|
||||||
}
|
}
|
||||||
|
SelectOptionsUpdate::FillColor(fill_choice) => {
|
||||||
|
apply_fill_color_pick(&mut self.drawing, fill_choice.clone(), context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::FillEnabled(enabled) => {
|
||||||
|
apply_fill_enabled(&mut self.drawing, *enabled, context.global_tool_data, context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::StrokeColor(color) => {
|
||||||
|
apply_stroke_color_pick(&mut self.drawing, *color, context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::StrokeEnabled(enabled) => {
|
||||||
|
apply_stroke_enabled(&mut self.drawing, *enabled, context.global_tool_data, context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::SwapFillAndStroke => {
|
||||||
|
swap_fill_and_stroke(&mut self.drawing, context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::StrokeOption(update) => {
|
||||||
|
apply_stroke_option(&mut self.drawing, update.clone(), context.document, responses);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
|
SelectOptionsUpdate::WorkingColorsChanged => {
|
||||||
|
apply_working_colors(&mut self.drawing, context.global_tool_data, context.document);
|
||||||
|
drawing_options_changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &(), responses, false);
|
self.fsm_state.process_event(message, &mut self.tool_data, context, &(), responses, false);
|
||||||
|
|
||||||
if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_pivot {
|
if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_pivot || drawing_options_changed {
|
||||||
// Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool)
|
// Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool)
|
||||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||||
self.tool_data.selected_layers_changed = false;
|
self.tool_data.selected_layers_changed = false;
|
||||||
|
|
@ -340,6 +447,8 @@ impl ToolTransition for SelectTool {
|
||||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||||
EventToMessageMap {
|
EventToMessageMap {
|
||||||
tool_abort: Some(SelectToolMessage::Abort.into()),
|
tool_abort: Some(SelectToolMessage::Abort.into()),
|
||||||
|
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||||
|
working_color_changed: Some(SelectToolMessage::WorkingColorChanged.into()),
|
||||||
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
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,
|
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, 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,
|
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::gizmos::gizmo_manager::GizmoManager;
|
||||||
|
|
@ -23,6 +23,7 @@ use crate::messages::tool::common_functionality::shapes::spiral_shape::Spiral;
|
||||||
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
|
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
|
||||||
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
|
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
|
||||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||||
use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool};
|
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::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
|
||||||
use crate::messages::tool::utility_types::DocumentToolData;
|
use crate::messages::tool::utility_types::DocumentToolData;
|
||||||
|
|
@ -93,7 +94,7 @@ impl Default for ShapeToolOptions {
|
||||||
pub enum ShapeOptionsUpdate {
|
pub enum ShapeOptionsUpdate {
|
||||||
FillColor(FillChoice),
|
FillColor(FillChoice),
|
||||||
FillEnabled(bool),
|
FillEnabled(bool),
|
||||||
LineWeight(f64),
|
StrokeOption(StrokeOptionsUpdate),
|
||||||
StrokeColor(Option<Color>),
|
StrokeColor(Option<Color>),
|
||||||
StrokeEnabled(bool),
|
StrokeEnabled(bool),
|
||||||
SwapFillAndStroke,
|
SwapFillAndStroke,
|
||||||
|
|
@ -239,29 +240,6 @@ fn create_arc_type_widget(arc_type: ArcType) -> WidgetInstance {
|
||||||
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance()
|
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance()
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
|
||||||
if let Some(value) = number_input.value {
|
|
||||||
ShapeToolMessage::UpdateOptions {
|
|
||||||
options: ShapeOptionsUpdate::LineWeight(value),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Message::NoOp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
|
||||||
.widget_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_arrow_shaft_width_widget(shaft_width: f64) -> WidgetInstance {
|
fn create_arrow_shaft_width_widget(shaft_width: f64) -> WidgetInstance {
|
||||||
NumberInput::new(Some(shaft_width))
|
NumberInput::new(Some(shaft_width))
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
|
|
@ -525,9 +503,13 @@ impl LayoutHolder for ShapeTool {
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
|
||||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
widgets.push(create_stroke_options_popover_widget(&self.options.drawing, weight_disabled, |update| {
|
||||||
|
ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::StrokeOption(update),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
|
||||||
// Shape-mode dropdown and per-shape parameters
|
// Shape-mode dropdown and per-shape parameters
|
||||||
if !self.tool_data.hide_shape_option_widget {
|
if !self.tool_data.hide_shape_option_widget {
|
||||||
|
|
@ -629,8 +611,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
|
||||||
}
|
}
|
||||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||||
}
|
}
|
||||||
ShapeOptionsUpdate::LineWeight(line_weight) => {
|
ShapeOptionsUpdate::StrokeOption(update) => {
|
||||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||||
}
|
}
|
||||||
ShapeOptionsUpdate::StrokeColor(color) => {
|
ShapeOptionsUpdate::StrokeColor(color) => {
|
||||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||||
|
|
@ -1176,7 +1158,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
skip_rerender: false,
|
skip_rerender: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||||
}
|
}
|
||||||
ShapeType::Arrow => {
|
ShapeType::Arrow => {
|
||||||
|
|
@ -1190,7 +1172,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
|
|
||||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||||
tool_data.line_data.editing_layer = Some(layer);
|
tool_data.line_data.editing_layer = Some(layer);
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||||
}
|
}
|
||||||
ShapeType::Line => {
|
ShapeType::Line => {
|
||||||
|
|
@ -1204,7 +1186,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
|
|
||||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||||
tool_data.line_data.editing_layer = Some(layer);
|
tool_data.line_data.editing_layer = Some(layer);
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_options.drawing.effective_line_weight(), layer, defered_responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
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::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
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,
|
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, reset_colors_on_deactivation, swap_fill_and_stroke,
|
||||||
swap_fill_and_stroke, sync_drawing_state,
|
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::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::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};
|
||||||
|
use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
|
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
|
|
@ -73,7 +74,7 @@ enum SplineToolFsmState {
|
||||||
pub enum SplineOptionsUpdate {
|
pub enum SplineOptionsUpdate {
|
||||||
FillColor(FillChoice),
|
FillColor(FillChoice),
|
||||||
FillEnabled(bool),
|
FillEnabled(bool),
|
||||||
LineWeight(f64),
|
StrokeOption(StrokeOptionsUpdate),
|
||||||
StrokeColor(Option<Color>),
|
StrokeColor(Option<Color>),
|
||||||
StrokeEnabled(bool),
|
StrokeEnabled(bool),
|
||||||
SwapFillAndStroke,
|
SwapFillAndStroke,
|
||||||
|
|
@ -92,29 +93,6 @@ impl ToolMetadata for SplineTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
|
||||||
if let Some(value) = number_input.value {
|
|
||||||
SplineToolMessage::UpdateOptions {
|
|
||||||
options: SplineOptionsUpdate::LineWeight(value),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Message::NoOp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_commit(|_| DocumentMessage::StartTransaction.into())
|
|
||||||
.widget_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutHolder for SplineTool {
|
impl LayoutHolder for SplineTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||||
|
|
@ -162,9 +140,13 @@ impl LayoutHolder for SplineTool {
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
|
||||||
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
let weight_disabled = self.options.drawing.stroke.enabled == Some(false);
|
||||||
widgets.push(create_weight_widget(self.options.drawing.line_weight, weight_disabled));
|
widgets.push(create_stroke_options_popover_widget(&self.options.drawing, weight_disabled, |update| {
|
||||||
|
SplineToolMessage::UpdateOptions {
|
||||||
|
options: SplineOptionsUpdate::StrokeOption(update),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}));
|
||||||
|
|
||||||
Layout(vec![LayoutGroup::row(widgets)])
|
Layout(vec![LayoutGroup::row(widgets)])
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +177,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Spli
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match options {
|
match options {
|
||||||
SplineOptionsUpdate::LineWeight(line_weight) => {
|
SplineOptionsUpdate::StrokeOption(update) => {
|
||||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||||
}
|
}
|
||||||
SplineOptionsUpdate::FillColor(fill_choice) => {
|
SplineOptionsUpdate::FillColor(fill_choice) => {
|
||||||
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
apply_fill_color_pick(&mut self.options.drawing, fill_choice, context.document, responses);
|
||||||
|
|
@ -425,7 +407,7 @@ impl Fsm for SplineToolFsmState {
|
||||||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||||
|
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
tool_options.drawing.stroke.apply_stroke(tool_data.weight, layer, responses);
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
tool_data.current_layer = Some(layer);
|
tool_data.current_layer = Some(layer);
|
||||||
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
export let tableAlign = false;
|
export let tableAlign = false;
|
||||||
// Sizing
|
// Sizing
|
||||||
export let minWidth = 0;
|
export let minWidth = 0;
|
||||||
|
export let maxWidth = 0;
|
||||||
export let minWidthCharacters = 0;
|
export let minWidthCharacters = 0;
|
||||||
// Tooltips
|
// Tooltips
|
||||||
export let tooltipLabel: string | undefined = undefined;
|
export let tooltipLabel: string | undefined = undefined;
|
||||||
|
|
@ -77,7 +78,8 @@
|
||||||
class:multiline
|
class:multiline
|
||||||
class:center-align={centerAlign}
|
class:center-align={centerAlign}
|
||||||
class:table-align={tableAlign}
|
class:table-align={tableAlign}
|
||||||
style:min-width={minWidthCharacters ? `${minWidthCharacters}ch` : minWidth || undefined}
|
style:min-width={minWidthCharacters ? `${minWidthCharacters}ch` : minWidth > 0 ? `${minWidth}px` : undefined}
|
||||||
|
style:max-width={maxWidth > 0 ? `${maxWidth}px` : undefined}
|
||||||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||||
data-tooltip-label={tooltipLabel}
|
data-tooltip-label={tooltipLabel}
|
||||||
data-tooltip-description={tooltipDescription}
|
data-tooltip-description={tooltipDescription}
|
||||||
|
|
|
||||||
|
|
@ -294,9 +294,10 @@ impl ClickTarget {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection point inside compound fill (non-zero rule)
|
// Selection point inside compound fill (non-zero rule).
|
||||||
let combined: BezPath = subpaths.iter().flat_map(|subpath| subpath.to_bezpath()).collect();
|
// Only closed subpaths contribute to the fill region; open segments would otherwise produce spurious winding on one side of the segment.
|
||||||
if bezier_iter().next().is_some_and(|bezier| combined.contains(bezier.start())) {
|
let combined: BezPath = subpaths.iter().filter(|subpath| subpath.closed()).flat_map(|subpath| subpath.to_bezpath()).collect();
|
||||||
|
if !combined.is_empty() && bezier_iter().next().is_some_and(|bezier| combined.contains(bezier.start())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue