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_shortcut == button2.tooltip_shortcut
|
||||
&& 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
|
||||
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
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
#[serde(rename = "minWidthCharacters")]
|
||||
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("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(),
|
||||
RadioInput::new(vec![
|
||||
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![
|
||||
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(),
|
||||
];
|
||||
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(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(),
|
||||
NumberInput::new(Some(grid.origin.x))
|
||||
.label("X")
|
||||
|
|
@ -306,7 +306,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
|
||||
match grid.grid_type {
|
||||
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(),
|
||||
NumberInput::new(Some(spacing.x))
|
||||
.label("X")
|
||||
|
|
@ -326,7 +326,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
])),
|
||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
|
||||
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(),
|
||||
NumberInput::new(Some(y_axis_spacing))
|
||||
.unit(" px")
|
||||
|
|
@ -336,7 +336,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
.widget_instance(),
|
||||
]));
|
||||
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(),
|
||||
NumberInput::new(Some(angle_a))
|
||||
.unit("°")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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;
|
||||
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
||||
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).
|
||||
/// 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,
|
||||
/// 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.
|
||||
pub last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||
/// The fill swatch's color, checkbox, and mixed state.
|
||||
|
|
@ -132,6 +144,13 @@ impl DrawingToolState {
|
|||
Self {
|
||||
line_weight: Some(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(),
|
||||
fill: if fill_enabled { ToolColorOptions::new_enabled() } else { ToolColorOptions::new_disabled() },
|
||||
stroke: ToolColorOptions::new_enabled(),
|
||||
|
|
@ -143,6 +162,33 @@ impl DrawingToolState {
|
|||
pub fn effective_line_weight(&self) -> f64 {
|
||||
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.
|
||||
|
|
@ -250,9 +296,72 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
|
|||
needs_refresh = true;
|
||||
}
|
||||
|
||||
needs_refresh |= sync_stroke_options(drawing, document);
|
||||
|
||||
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.
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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, FillChoice, Gradient};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
||||
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.
|
||||
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))
|
||||
|
|
@ -759,9 +819,10 @@ impl<'a> NodeGraphLayer<'a> {
|
|||
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> {
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ pub mod resize;
|
|||
pub mod shape_editor;
|
||||
pub mod shapes;
|
||||
pub mod snapping;
|
||||
pub mod stroke_options;
|
||||
pub mod transformation_cage;
|
||||
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::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
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,
|
||||
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,
|
||||
sync_drawing_state,
|
||||
};
|
||||
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 glam::DVec2;
|
||||
use graph_craft::document::NodeId;
|
||||
|
|
@ -58,7 +59,7 @@ pub enum FreehandToolMessage {
|
|||
pub enum FreehandOptionsUpdate {
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeOption(StrokeOptionsUpdate),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
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 {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||
|
|
@ -154,9 +132,13 @@ impl LayoutHolder for FreehandTool {
|
|||
.into()
|
||||
},
|
||||
));
|
||||
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));
|
||||
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)])
|
||||
}
|
||||
|
|
@ -193,8 +175,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
|
|||
FreehandOptionsUpdate::FillEnabled(enabled) => {
|
||||
apply_fill_enabled(&mut self.options.drawing, enabled, context.global_tool_data, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::LineWeight(line_weight) => {
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
FreehandOptionsUpdate::StrokeOption(update) => {
|
||||
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||
}
|
||||
FreehandOptionsUpdate::StrokeColor(color) => {
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
|
|
@ -244,7 +226,6 @@ impl ToolTransition for FreehandTool {
|
|||
struct FreehandToolData {
|
||||
end_point: Option<(DVec2, PointId)>,
|
||||
dragged: bool,
|
||||
weight: f64,
|
||||
layer: Option<LayerNodeIdentifier>,
|
||||
/// Viewport-space start position for newly created layers, used to compute local-space
|
||||
/// positions before the deferred TransformSet has been reflected in metadata.
|
||||
|
|
@ -283,7 +264,6 @@ impl Fsm for FreehandToolFsmState {
|
|||
|
||||
tool_data.dragged = false;
|
||||
tool_data.end_point = None;
|
||||
tool_data.weight = tool_options.drawing.effective_line_weight();
|
||||
tool_data.new_layer_viewport_start = None;
|
||||
|
||||
// Extend an endpoint of the selected path
|
||||
|
|
@ -322,7 +302,7 @@ impl Fsm for FreehandToolFsmState {
|
|||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
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_data.layer = Some(layer);
|
||||
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::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::stroke_options::StrokeOptionsUpdate;
|
||||
use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -776,7 +757,7 @@ mod test_freehand {
|
|||
let custom_line_weight = 5.;
|
||||
editor
|
||||
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::LineWeight(custom_line_weight),
|
||||
options: FreehandOptionsUpdate::StrokeOption(StrokeOptionsUpdate::LineWeight(custom_line_weight)),
|
||||
}))
|
||||
.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::tool::common_functionality::auto_panning::AutoPanning;
|
||||
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,
|
||||
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,
|
||||
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};
|
||||
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 graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
|
|
@ -121,7 +122,7 @@ pub enum PenOverlayMode {
|
|||
pub enum PenOptionsUpdate {
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeOption(StrokeOptionsUpdate),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
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 {
|
||||
fn layout(&self) -> Layout {
|
||||
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);
|
||||
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());
|
||||
|
||||
|
|
@ -277,8 +258,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenT
|
|||
self.options.pen_overlay_mode = overlay_mode_type;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PenOptionsUpdate::LineWeight(line_weight) => {
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
PenOptionsUpdate::StrokeOption(update) => {
|
||||
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||
}
|
||||
PenOptionsUpdate::FillColor(fill_choice) => {
|
||||
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 layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
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);
|
||||
self.prior_segment = 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::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{
|
||||
DrawingToolState, apply_fill_color_pick, apply_fill_enabled, apply_stroke_color_pick, apply_stroke_enabled, apply_working_colors, swap_fill_and_stroke, sync_drawing_state,
|
||||
};
|
||||
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::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::shape_editor::SelectionShapeType;
|
||||
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::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
|
||||
use glam::DMat2;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
#[derive(ExtractField)]
|
||||
pub struct SelectTool {
|
||||
fsm_state: SelectToolFsmState,
|
||||
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)]
|
||||
|
|
@ -41,12 +58,19 @@ pub struct SelectOptions {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
NestedSelectionBehavior(NestedSelectionBehavior),
|
||||
PivotGizmoType(PivotGizmoType),
|
||||
SetPivotGizmoEnabled(bool),
|
||||
TogglePivotPinned,
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
SwapFillAndStroke,
|
||||
StrokeOption(StrokeOptionsUpdate),
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
|
|
@ -81,6 +105,8 @@ pub struct SelectToolPointerKeys {
|
|||
pub enum SelectToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
SelectionChanged,
|
||||
WorkingColorChanged,
|
||||
Overlays {
|
||||
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<> {
|
||||
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
|
||||
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
|
||||
|
|
@ -222,6 +236,65 @@ impl LayoutHolder for SelectTool {
|
|||
fn layout(&self) -> Layout {
|
||||
let mut widgets = Vec::new();
|
||||
|
||||
// Fill/Stroke widget set (only shown when there's a selection to apply edits to)
|
||||
if self.tool_data.selected_layers_count > 0 {
|
||||
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)
|
||||
widgets.push(self.deep_selection_widget());
|
||||
|
||||
|
|
@ -259,10 +332,6 @@ impl LayoutHolder for SelectTool {
|
|||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.extend(self.flip_widgets(disabled));
|
||||
|
||||
// Turn
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.extend(self.turn_widgets(disabled));
|
||||
|
||||
// Boolean
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
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 {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let mut redraw_reference_pivot = false;
|
||||
let mut drawing_options_changed = false;
|
||||
|
||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: ref option_update }) = message {
|
||||
match *option_update {
|
||||
if matches!(&message, ToolMessage::Select(SelectToolMessage::SelectionChanged)) && sync_drawing_state(&mut self.drawing, true, true, context.global_tool_data, context.document) {
|
||||
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) => {
|
||||
self.tool_data.nested_selection_behavior = nested_selection_behavior;
|
||||
self.tool_data.nested_selection_behavior = *nested_selection_behavior;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
}
|
||||
SelectOptionsUpdate::PivotGizmoType(gizmo_type) => {
|
||||
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);
|
||||
let pivot_gizmo = self.tool_data.pivot_gizmo();
|
||||
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
||||
|
|
@ -293,24 +373,51 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
|
|||
}
|
||||
}
|
||||
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(NodeGraphMessage::RunDocumentGraph);
|
||||
redraw_reference_pivot = true;
|
||||
}
|
||||
|
||||
SelectOptionsUpdate::TogglePivotPinned => {
|
||||
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
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);
|
||||
|
||||
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)
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
self.tool_data.selected_layers_changed = false;
|
||||
|
|
@ -340,6 +447,8 @@ impl ToolTransition for SelectTool {
|
|||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
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()),
|
||||
..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::tool::common_functionality::auto_panning::AutoPanning;
|
||||
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,
|
||||
};
|
||||
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::{Ellipse, Line, Rectangle};
|
||||
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::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
|
||||
use crate::messages::tool::utility_types::DocumentToolData;
|
||||
|
|
@ -93,7 +94,7 @@ impl Default for ShapeToolOptions {
|
|||
pub enum ShapeOptionsUpdate {
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeOption(StrokeOptionsUpdate),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
NumberInput::new(Some(shaft_width))
|
||||
.unit(" px")
|
||||
|
|
@ -525,9 +503,13 @@ impl LayoutHolder for ShapeTool {
|
|||
.into()
|
||||
},
|
||||
));
|
||||
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));
|
||||
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
|
||||
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);
|
||||
}
|
||||
ShapeOptionsUpdate::LineWeight(line_weight) => {
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
ShapeOptionsUpdate::StrokeOption(update) => {
|
||||
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||
}
|
||||
ShapeOptionsUpdate::StrokeColor(color) => {
|
||||
apply_stroke_color_pick(&mut self.options.drawing, color, context.document, responses);
|
||||
|
|
@ -1176,7 +1158,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
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);
|
||||
}
|
||||
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.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);
|
||||
}
|
||||
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.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::tool::common_functionality::auto_panning::AutoPanning;
|
||||
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,
|
||||
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,
|
||||
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::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
|
@ -73,7 +74,7 @@ enum SplineToolFsmState {
|
|||
pub enum SplineOptionsUpdate {
|
||||
FillColor(FillChoice),
|
||||
FillEnabled(bool),
|
||||
LineWeight(f64),
|
||||
StrokeOption(StrokeOptionsUpdate),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeEnabled(bool),
|
||||
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 {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.drawing.fill.create_widgets(
|
||||
|
|
@ -162,9 +140,13 @@ impl LayoutHolder for SplineTool {
|
|||
.into()
|
||||
},
|
||||
));
|
||||
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));
|
||||
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)])
|
||||
}
|
||||
|
|
@ -195,8 +177,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Spli
|
|||
return;
|
||||
};
|
||||
match options {
|
||||
SplineOptionsUpdate::LineWeight(line_weight) => {
|
||||
apply_line_weight(&mut self.options.drawing, line_weight, context.document, responses);
|
||||
SplineOptionsUpdate::StrokeOption(update) => {
|
||||
apply_stroke_option(&mut self.options.drawing, update, context.document, responses);
|
||||
}
|
||||
SplineOptionsUpdate::FillColor(fill_choice) => {
|
||||
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 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_data.current_layer = Some(layer);
|
||||
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
export let tableAlign = false;
|
||||
// Sizing
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
export let minWidthCharacters = 0;
|
||||
// Tooltips
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
|
|
@ -77,7 +78,8 @@
|
|||
class:multiline
|
||||
class:center-align={centerAlign}
|
||||
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}
|
||||
data-tooltip-label={tooltipLabel}
|
||||
data-tooltip-description={tooltipDescription}
|
||||
|
|
|
|||
|
|
@ -294,9 +294,10 @@ impl ClickTarget {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Selection point inside compound fill (non-zero rule)
|
||||
let combined: BezPath = subpaths.iter().flat_map(|subpath| subpath.to_bezpath()).collect();
|
||||
if bezier_iter().next().is_some_and(|bezier| combined.contains(bezier.start())) {
|
||||
// Selection point inside compound fill (non-zero rule).
|
||||
// Only closed subpaths contribute to the fill region; open segments would otherwise produce spurious winding on one side of the segment.
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue