From 7e1b452757d420efad9271eba4e312c80d2543d7 Mon Sep 17 00:00:00 2001 From: Chase <60807752+Chase-Percy@users.noreply.github.com> Date: Mon, 8 May 2023 18:00:10 +0800 Subject: [PATCH] Add color choices to the options bar of tools (#1199) * Generalize PenColorType -> ToolColorType * impl default for ToolColorOptions * Add stroke color option to the freehand tool * Consolidate working color update messages * Update tool working colours when switching tools * Update working colors on tool activation * Add stroke color option to line tool * Add fill color option to freehand tool * Add tool color options to spline tool * Fix freehand tool * Add color options to text * Add tool color/weight options to rectangle * Add tool color/weight options to ellipse * Add tool color/weight options to shape * Fix spline default fill/stroke * Reorder widgets and code cleanup * Add CustomColor icon * Fix warnings * Change color defaults to secondary fill, primary stroke * Fix spacing between brush options number inputs * Add toolbar color option to brush * Implement allowNone on color input widget * Rearrange widget and remove X from brush --------- Co-authored-by: Keavon Chambers --- .../layout/utility_types/layout_widget.rs | 3 + .../utility_types/widgets/input_widgets.rs | 8 +- .../node_properties.rs | 2 +- .../common_functionality/color_selector.rs | 106 +++++++++++ .../messages/tool/common_functionality/mod.rs | 1 + .../src/messages/tool/tool_message_handler.rs | 3 + .../messages/tool/tool_messages/brush_tool.rs | 100 +++++++--- .../tool/tool_messages/ellipse_tool.rs | 128 ++++++++++++- .../tool/tool_messages/freehand_tool.rs | 113 +++++++++-- .../tool/tool_messages/gradient_tool.rs | 1 + .../messages/tool/tool_messages/line_tool.rs | 69 ++++++- .../messages/tool/tool_messages/pen_tool.rs | 180 ++++-------------- .../tool/tool_messages/rectangle_tool.rs | 128 ++++++++++++- .../tool/tool_messages/select_tool.rs | 7 +- .../messages/tool/tool_messages/shape_tool.rs | 130 +++++++++++-- .../tool/tool_messages/spline_tool.rs | 109 +++++++++-- .../messages/tool/tool_messages/text_tool.rs | 138 +++++++++----- .../assets/icon-16px-solid/custom-color.svg | 4 + .../widgets/inputs/ColorInput.svelte | 4 +- frontend/src/utility-functions/icons.ts | 2 + frontend/src/wasm-communication/messages.ts | 3 +- 21 files changed, 929 insertions(+), 310 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/color_selector.rs create mode 100644 frontend/assets/icon-16px-solid/custom-color.svg diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index c56f6751..086f3d18 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -443,6 +443,9 @@ impl WidgetHolder { pub fn new(widget: Widget) -> Self { Self { widget_id: generate_uuid(), widget } } + pub fn section_separator() -> Self { + Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder() + } pub fn unrelated_separator() -> Self { Separator::new(SeparatorDirection::Horizontal, SeparatorType::Unrelated).widget_holder() } diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 54e15fcf..161644d1 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -53,11 +53,9 @@ pub struct ColorInput { // #[serde(rename = "allowTransparency")] // #[derivative(Default(value = "false"))] // pub allow_transparency: bool, - - // TODO: Implement - // #[serde(rename = "allowNone")] - // #[derivative(Default(value = "false"))] - // pub allow_none: bool, + #[serde(rename = "allowNone")] + #[derivative(Default(value = "true"))] + pub allow_none: bool, // pub disabled: bool, pub tooltip: String, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 237903d5..aa1705d3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -588,7 +588,7 @@ pub fn blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _con } pub fn brush_node_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let color = color_widget(document_node, node_id, 7, "Color", ColorInput::default(), true); + let color = color_widget(document_node, node_id, 7, "Color", ColorInput::default().allow_none(false), true); let size = number_widget(document_node, node_id, 4, "Diameter", NumberInput::default().min(1.).max(100.).unit(" px"), true); let hardness = number_widget(document_node, node_id, 5, "Hardness", NumberInput::default().min(0.).max(100.).unit("%"), true); diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs new file mode 100644 index 00000000..8d3ffbfb --- /dev/null +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -0,0 +1,106 @@ +use crate::messages::layout::utility_types::layout_widget::WidgetCallback; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, IconButton, RadioEntryData, RadioInput, TextLabel, WidgetHolder}; + +use graphene_core::Color; + +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum ToolColorType { + Primary, + Secondary, + Custom, +} + +pub struct ToolColorOptions { + pub custom_color: Option, + pub primary_working_color: Option, + pub secondary_working_color: Option, + pub color_type: ToolColorType, +} + +impl Default for ToolColorOptions { + fn default() -> Self { + Self { + color_type: ToolColorType::Primary, + custom_color: Some(Color::BLACK), + primary_working_color: Some(Color::BLACK), + secondary_working_color: Some(Color::WHITE), + } + } +} + +impl ToolColorOptions { + pub fn new_primary() -> Self { + Self::default() + } + + pub fn new_secondary() -> Self { + Self { + color_type: ToolColorType::Secondary, + ..Default::default() + } + } + + pub fn new_none() -> Self { + Self { + color_type: ToolColorType::Custom, + custom_color: None, + ..Default::default() + } + } + + pub fn active_color(&self) -> Option { + match self.color_type { + ToolColorType::Custom => self.custom_color, + ToolColorType::Primary => self.primary_working_color, + ToolColorType::Secondary => self.secondary_working_color, + } + } + + pub fn create_widgets( + &self, + label_text: impl Into, + color_allow_none: bool, + reset_callback: WidgetCallback, + radio_callback: fn(ToolColorType) -> WidgetCallback<()>, + color_callback: WidgetCallback, + ) -> Vec { + let mut widgets = vec![TextLabel::new(label_text).widget_holder()]; + + if !color_allow_none { + widgets.push(WidgetHolder::unrelated_separator()); + } else { + let mut reset = IconButton::new("CloseX", 12) + .disabled(self.custom_color.is_none() && self.color_type == ToolColorType::Custom) + .tooltip("Clear Color"); + reset.on_update = reset_callback; + + widgets.push(WidgetHolder::related_separator()); + widgets.push(reset.widget_holder()); + widgets.push(WidgetHolder::related_separator()); + }; + + let entries = vec![ + ("WorkingColorsPrimary", "Primary Working Color", ToolColorType::Primary), + ("WorkingColorsSecondary", "Secondary Working Color", ToolColorType::Secondary), + ("CustomColor", "Custom Color", ToolColorType::Custom), + ] + .into_iter() + .map(|(icon, tooltip, color_type)| { + let mut entry = RadioEntryData::new("").tooltip(tooltip).icon(icon); + entry.on_update = radio_callback(color_type); + entry + }) + .collect(); + let radio = RadioInput::new(entries).selected_index(self.color_type.clone() as u32).widget_holder(); + widgets.push(radio); + widgets.push(WidgetHolder::related_separator()); + + let mut color_input = ColorInput::new(self.active_color()).allow_none(color_allow_none); + color_input.on_update = color_callback; + widgets.push(color_input.widget_holder()); + + widgets + } +} diff --git a/editor/src/messages/tool/common_functionality/mod.rs b/editor/src/messages/tool/common_functionality/mod.rs index e79ef0a9..1ad7dd88 100644 --- a/editor/src/messages/tool/common_functionality/mod.rs +++ b/editor/src/messages/tool/common_functionality/mod.rs @@ -1,3 +1,4 @@ +pub mod color_selector; pub mod graph_modification_utils; pub mod overlay_renderer; pub mod path_outline; diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 600f3ecd..d29adf92 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -129,6 +129,9 @@ impl MessageHandler), + ColorType(ToolColorType), Diameter(f64), Flow(f64), Hardness(f64), + WorkingColors(Option, Option), } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -85,32 +94,42 @@ impl ToolMetadata for BrushTool { impl PropertyHolder for BrushTool { fn properties(&self) -> Layout { - let diameter = NumberInput::new(Some(self.options.diameter)) - .label("Diameter") - .min(1.) - .unit(" px") - .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into()) - .widget_holder(); - let hardness = NumberInput::new(Some(self.options.hardness)) - .label("Hardness") - .min(0.) - .max(100.) - .unit("%") - .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into()) - .widget_holder(); - let flow = NumberInput::new(Some(self.options.flow)) - .label("Flow") - .min(1.) - .max(100.) - .unit("%") - .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) - .widget_holder(); + let mut widgets = vec![ + NumberInput::new(Some(self.options.diameter)) + .label("Diameter") + .min(1.) + .unit(" px") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into()) + .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(Some(self.options.hardness)) + .label("Hardness") + .min(0.) + .max(100.) + .unit("%") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into()) + .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(Some(self.options.flow)) + .label("Flow") + .min(1.) + .max(100.) + .unit("%") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) + .widget_holder(), + ]; - let separator = Separator::new(SeparatorDirection::Horizontal, SeparatorType::Related).widget_holder(); + widgets.push(WidgetHolder::section_separator()); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { - widgets: vec![diameter, separator.clone(), hardness, separator, flow], - }])) + widgets.append(&mut self.options.color.create_widgets( + "Color", + false, + WidgetCallback::new(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into()), + )); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -133,7 +152,22 @@ impl<'a> MessageHandler> for BrushTo BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness, BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, + BrushToolMessageOptionsUpdate::Color(color) => { + self.options.color.custom_color = color; + self.options.color.color_type = ToolColorType::Custom; + } + BrushToolMessageOptionsUpdate::ColorType(color_type) => self.options.color.color_type = color_type, + BrushToolMessageOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.color.primary_working_color = primary; + self.options.color.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -164,6 +198,7 @@ impl ToolTransition for BrushTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(BrushToolMessage::Abort.into()), + working_color_changed: Some(BrushToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -247,7 +282,7 @@ impl Fsm for BrushToolFsmState { tool_data.points.push(vec![pos]); if new_layer { - add_brush_render(tool_options, tool_data, global_tool_data, responses); + add_brush_render(tool_options, tool_data, responses); } else { //tool_data.update_image(node_graph, responses); tool_data.update_points(responses); @@ -291,6 +326,13 @@ impl Fsm for BrushToolFsmState { Ready } + (_, WorkingColorChanged) => { + responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { @@ -312,7 +354,7 @@ impl Fsm for BrushToolFsmState { } } -fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque) { +fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque) { let layer_path = data.path.clone().unwrap(); let brush_node = DocumentNode { @@ -329,7 +371,7 @@ fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, tool_data // Flow NodeInput::value(TaggedValue::F64(tool_options.flow), false), // Color - NodeInput::value(TaggedValue::Color(tool_data.primary_color), false), + NodeInput::value(TaggedValue::Color(tool_options.color.active_color().unwrap()), false), ], implementation: DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()), metadata: graph_craft::document::DocumentNodeMetadata { position: (8, 4).into() }, diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index e28bf8d8..6f5ada2d 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -1,13 +1,17 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, NumberInput, WidgetHolder}; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; -use graphene_core::vector::style::Fill; +use graphene_core::vector::style::{Fill, Stroke}; +use graphene_core::Color; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -16,15 +20,45 @@ use serde::{Deserialize, Serialize}; pub struct EllipseTool { fsm_state: EllipseToolFsmState, data: EllipseToolData, + options: EllipseToolOptions, +} + +pub struct EllipseToolOptions { + line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, +} + +impl Default for EllipseToolOptions { + fn default() -> Self { + Self { + line_weight: 5., + fill: ToolColorOptions::new_secondary(), + stroke: ToolColorOptions::new_primary(), + } + } +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum EllipseOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), + LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } #[remain::sorted] #[impl_message(Message, ToolMessage, Ellipse)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum EllipseToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages DragStart, @@ -33,6 +67,7 @@ pub enum EllipseToolMessage { center: Key, lock_ratio: Key, }, + UpdateOptions(EllipseOptionsUpdate), } impl ToolMetadata for EllipseTool { @@ -47,11 +82,73 @@ impl ToolMetadata for EllipseTool { } } -impl PropertyHolder for EllipseTool {} +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .on_update(|number_input: &NumberInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + +impl PropertyHolder for EllipseTool { + fn properties(&self) -> Layout { + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value)).into()), + ); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value)).into()), + )); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} impl<'a> MessageHandler> for EllipseTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - self.fsm_state.process_event(message, &mut self.data, tool_data, &(), responses, true); + if let ToolMessage::Ellipse(EllipseToolMessage::UpdateOptions(action)) = message { + match action { + EllipseOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + EllipseOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + EllipseOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + EllipseOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + EllipseOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + EllipseOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } + } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + + return; + } + + self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true); } fn actions(&self) -> ActionList { @@ -74,6 +171,7 @@ impl ToolTransition for EllipseTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(EllipseToolMessage::Abort.into()), + working_color_changed: Some(EllipseToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -93,7 +191,7 @@ struct EllipseToolData { impl Fsm for EllipseToolFsmState { type ToolData = EllipseToolData; - type ToolOptions = (); + type ToolOptions = EllipseToolOptions; fn transition( self, @@ -106,7 +204,7 @@ impl Fsm for EllipseToolFsmState { render_data, .. }: &mut ToolActionHandlerData, - _tool_options: &Self::ToolOptions, + tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { use EllipseToolFsmState::*; @@ -130,10 +228,15 @@ impl Fsm for EllipseToolFsmState { graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); graph_modification_utils::set_manipulator_mirror_angle(&manipulator_groups, &layer_path, true, responses); - // Set the fill color to the primary working color + let fill_color = tool_options.fill.active_color(); responses.add(GraphOperationMessage::FillSet { + layer: layer_path.clone(), + fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None }, + }); + + responses.add(GraphOperationMessage::StrokeSet { layer: layer_path, - fill: Fill::solid(global_tool_data.primary_color), + stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight), }); Drawing @@ -157,6 +260,13 @@ impl Fsm for EllipseToolFsmState { Ready } + (_, WorkingColorChanged) => { + responses.add(EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index 1cac7f63..72a03174 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -1,15 +1,19 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; -use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; +use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::LayerId; use document_legacy::Operation; -use graphene_core::vector::style::Stroke; +use graphene_core::vector::style::{Fill, Stroke}; +use graphene_core::Color; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -23,11 +27,17 @@ pub struct FreehandTool { pub struct FreehandOptions { line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, } impl Default for FreehandOptions { fn default() -> Self { - Self { line_weight: 5. } + Self { + line_weight: 5., + fill: ToolColorOptions::new_none(), + stroke: ToolColorOptions::new_primary(), + } } } @@ -38,18 +48,25 @@ pub enum FreehandToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages DragStart, DragStop, PointerMove, - UpdateOptions(FreehandToolMessageOptionsUpdate), + UpdateOptions(FreehandOptionsUpdate), } #[remain::sorted] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] -pub enum FreehandToolMessageOptionsUpdate { +pub enum FreehandOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -71,15 +88,38 @@ impl ToolMetadata for FreehandTool { } } +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(1.) + .on_update(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + impl PropertyHolder for FreehandTool { fn properties(&self) -> Layout { - let weight = NumberInput::new(Some(self.options.line_weight)) - .unit(" px") - .label("Weight") - .min(1.) - .on_update(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![weight] }])) + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value)).into()), + ); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value)).into()), + )); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -87,8 +127,30 @@ impl<'a> MessageHandler> for Freehan fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { if let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(action)) = message { match action { - FreehandToolMessageOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + FreehandOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + FreehandOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + FreehandOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + FreehandOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + FreehandOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + FreehandOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -117,6 +179,7 @@ impl ToolTransition for FreehandTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(FreehandToolMessage::Abort.into()), + working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -161,7 +224,7 @@ impl Fsm for FreehandToolFsmState { tool_data.weight = tool_options.line_weight; - add_polyline(tool_data, global_tool_data, responses); + add_polyline(tool_data, tool_options.stroke.active_color(), tool_options.fill.active_color(), responses); Drawing } @@ -172,14 +235,14 @@ impl Fsm for FreehandToolFsmState { tool_data.points.push(pos); } - add_polyline(tool_data, global_tool_data, responses); + add_polyline(tool_data, tool_options.stroke.active_color(), tool_options.fill.active_color(), responses); Drawing } (Drawing, DragStop) | (Drawing, Abort) => { if tool_data.points.len() >= 2 { responses.add(remove_preview(tool_data)); - add_polyline(tool_data, global_tool_data, responses); + add_polyline(tool_data, tool_options.stroke.active_color(), tool_options.fill.active_color(), responses); responses.add(DocumentMessage::CommitTransaction); } else { responses.add(DocumentMessage::AbortTransaction); @@ -190,6 +253,13 @@ impl Fsm for FreehandToolFsmState { Ready } + (_, FreehandToolMessage::WorkingColorChanged) => { + responses.add(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { @@ -215,14 +285,19 @@ fn remove_preview(data: &FreehandToolData) -> Message { Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() } -fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses: &mut VecDeque) { +fn add_polyline(data: &FreehandToolData, stroke_color: Option, fill_color: Option, responses: &mut VecDeque) { let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false); let layer_path = data.path.clone().unwrap(); graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); + responses.add(GraphOperationMessage::FillSet { + layer: layer_path.clone(), + fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None }, + }); + responses.add(GraphOperationMessage::StrokeSet { layer: layer_path, - stroke: Stroke::new(Some(tool_data.primary_color), data.weight), + stroke: Stroke::new(stroke_color, data.weight), }); } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 8869b095..613aeeda 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -117,6 +117,7 @@ impl PropertyHolder for GradientTool { ]) .selected_index((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32) .widget_holder(); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![gradient_type] }])) } } diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index 2b24d08e..8eda4e4b 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -2,9 +2,12 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; @@ -12,6 +15,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::LayerId; use graphene_core::vector::style::Stroke; +use graphene_core::Color; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -25,11 +29,15 @@ pub struct LineTool { pub struct LineOptions { line_weight: f64, + stroke: ToolColorOptions, } impl Default for LineOptions { fn default() -> Self { - Self { line_weight: 5. } + Self { + line_weight: 5., + stroke: ToolColorOptions::new_primary(), + } } } @@ -40,6 +48,8 @@ pub enum LineToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages DragStart, @@ -56,6 +66,9 @@ pub enum LineToolMessage { #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum LineOptionsUpdate { LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } impl ToolMetadata for LineTool { @@ -70,15 +83,28 @@ impl ToolMetadata for LineTool { } } +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .on_update(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + impl PropertyHolder for LineTool { fn properties(&self) -> Layout { - let weight = NumberInput::new(Some(self.options.line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .on_update(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![weight] }])) + let mut widgets = self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value)).into()), + ); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -87,7 +113,22 @@ impl<'a> MessageHandler> for LineToo if let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message { match action { LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + LineOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + LineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + LineOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -106,6 +147,7 @@ impl ToolTransition for LineTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(LineToolMessage::Abort.into()), + working_color_changed: Some(LineToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -164,7 +206,7 @@ impl Fsm for LineToolFsmState { graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); responses.add(GraphOperationMessage::StrokeSet { layer: layer_path, - stroke: Stroke::new(Some(global_tool_data.primary_color), tool_options.line_weight), + stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight), }); tool_data.weight = tool_options.line_weight; @@ -192,6 +234,13 @@ impl Fsm for LineToolFsmState { tool_data.path = None; Ready } + (_, WorkingColorChanged) => { + responses.add(LineToolMessage::UpdateOptions(LineOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 0eef81d3..98013faa 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,12 +1,13 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widget_prelude::{ColorInput, IconButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextLabel, WidgetHolder}; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; use crate::messages::portfolio::document::node_graph::VectorDataModification; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer; @@ -30,52 +31,18 @@ pub struct PenTool { options: PenOptions, } -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] -pub enum PenColorType { - Primary, - Secondary, - Custom, -} - -pub struct PenColorOptions { - color: Option, - primary_working_color: Option, - secondary_working_color: Option, - color_type: PenColorType, -} - -impl PenColorOptions { - pub fn active_color(&self) -> Option { - match self.color_type { - PenColorType::Custom => self.color, - PenColorType::Primary => self.primary_working_color, - PenColorType::Secondary => self.secondary_working_color, - } - } -} - pub struct PenOptions { line_weight: f64, - fill: PenColorOptions, - stroke: PenColorOptions, + fill: ToolColorOptions, + stroke: ToolColorOptions, } impl Default for PenOptions { fn default() -> Self { Self { line_weight: 5., - fill: PenColorOptions { - color: Some(Color::BLACK), - primary_working_color: Some(Color::BLACK), - secondary_working_color: Some(Color::WHITE), - color_type: PenColorType::Primary, - }, - stroke: PenColorOptions { - color: None, - primary_working_color: Some(Color::BLACK), - secondary_working_color: Some(Color::WHITE), - color_type: PenColorType::Custom, - }, + fill: ToolColorOptions::new_secondary(), + stroke: ToolColorOptions::new_primary(), } } } @@ -119,12 +86,11 @@ enum PenToolFsmState { #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum PenOptionsUpdate { FillColor(Option), - FillColorType(PenColorType), + FillColorType(ToolColorType), LineWeight(f64), - PrimaryColor(Option), - SecondaryColor(Option), StrokeColor(Option), - StrokeColorType(PenColorType), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } impl ToolMetadata for PenTool { @@ -139,85 +105,6 @@ impl ToolMetadata for PenTool { } } -// TODO: Generalize create_fill_widget and create_stroke_widget into one function. -fn create_fill_widget(fill: &PenColorOptions) -> Vec { - let label = TextLabel::new("Fill").widget_holder(); - - let reset = IconButton::new("CloseX", 12) - .disabled(fill.color.is_none() && fill.color_type == PenColorType::Custom) - .on_update(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into()) - .tooltip("Clear color") - .widget_holder(); - - let entries = vec![ - ("WorkingColorsPrimary", "Primary Working Color", PenColorType::Primary), - ("WorkingColorsSecondary", "Secondary Working Color", PenColorType::Secondary), - ("Edit", "Custom Color", PenColorType::Custom), - ] - .into_iter() - .map(|(icon, tooltip, color_type)| { - RadioEntryData::new("") - .tooltip(tooltip) - .icon(icon) - .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()) - }) - .collect(); - let radio = RadioInput::new(entries).selected_index(fill.color_type.clone() as u32).widget_holder(); - - let color_input = ColorInput::new(fill.active_color()) - .on_update(|fill_color| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(fill_color.value)).into()) - .widget_holder(); - - vec![ - label, - WidgetHolder::related_separator(), - reset, - WidgetHolder::related_separator(), - radio, - WidgetHolder::related_separator(), - color_input, - ] -} - -fn create_stroke_widget(stroke: &PenColorOptions) -> Vec { - let label = TextLabel::new("Stroke").widget_holder(); - - let reset = IconButton::new("CloseX", 12) - .disabled(stroke.color.is_none() && stroke.color_type == PenColorType::Custom) - .on_update(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into()) - .tooltip("Clear color") - .widget_holder(); - - let entries = vec![ - ("WorkingColorsPrimary", "Primary Working Color", PenColorType::Primary), - ("WorkingColorsSecondary", "Secondary Working Color", PenColorType::Secondary), - ("Edit", "Custom Color", PenColorType::Custom), - ] - .into_iter() - .map(|(icon, tooltip, color_type)| { - RadioEntryData::new("") - .tooltip(tooltip) - .icon(icon) - .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()) - }) - .collect(); - let radio = RadioInput::new(entries).selected_index(stroke.color_type.clone() as u32).widget_holder(); - - let color_input = ColorInput::new(stroke.active_color()) - .on_update(|stroke_color| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(stroke_color.value)).into()) - .widget_holder(); - - vec![ - label, - WidgetHolder::related_separator(), - reset, - WidgetHolder::related_separator(), - radio, - WidgetHolder::related_separator(), - color_input, - ] -} - fn create_weight_widget(line_weight: f64) -> WidgetHolder { NumberInput::new(Some(line_weight)) .unit(" px") @@ -229,11 +116,26 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder { impl PropertyHolder for PenTool { fn properties(&self) -> Layout { - let mut widgets = create_fill_widget(&self.options.fill); - widgets.push(Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder()); - widgets.append(&mut create_stroke_widget(&self.options.stroke)); + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value)).into()), + ); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value)).into()), + )); widgets.push(WidgetHolder::unrelated_separator()); widgets.push(create_weight_widget(self.options.line_weight)); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -244,22 +146,20 @@ impl<'a> MessageHandler> for PenTool match action { PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, PenOptionsUpdate::FillColor(color) => { - self.options.fill.color = color; - self.options.fill.color_type = PenColorType::Custom; + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; } PenOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, PenOptionsUpdate::StrokeColor(color) => { - self.options.stroke.color = color; - self.options.stroke.color_type = PenColorType::Custom; + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; } PenOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - PenOptionsUpdate::PrimaryColor(color) => { - self.options.stroke.primary_working_color = color; - self.options.fill.primary_working_color = color; - } - PenOptionsUpdate::SecondaryColor(color) => { - self.options.stroke.secondary_working_color = color; - self.options.fill.secondary_working_color = color; + PenOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; } } @@ -701,8 +601,10 @@ impl Fsm for PenToolFsmState { self } (_, PenToolMessage::WorkingColorChanged) => { - responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::PrimaryColor(Some(global_tool_data.primary_color)))); - responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::SecondaryColor(Some(global_tool_data.secondary_color)))); + responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); self } (PenToolFsmState::Ready, PenToolMessage::DragStart) => { diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index d4c112d6..b68823a6 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -1,29 +1,63 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::PropertyHolder; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, NumberInput, WidgetHolder}; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::DVec2; -use graphene_core::vector::style::Fill; +use graphene_core::vector::style::{Fill, Stroke}; +use graphene_core::Color; use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct RectangleTool { fsm_state: RectangleToolFsmState, tool_data: RectangleToolData, + options: RectangleToolOptions, +} + +pub struct RectangleToolOptions { + line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, +} + +impl Default for RectangleToolOptions { + fn default() -> Self { + Self { + line_weight: 5., + fill: ToolColorOptions::new_secondary(), + stroke: ToolColorOptions::new_primary(), + } + } +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub enum RectangleOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), + LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } #[remain::sorted] #[impl_message(Message, ToolMessage, Rectangle)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum RectangleToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages DragStart, @@ -32,13 +66,76 @@ pub enum RectangleToolMessage { center: Key, lock_ratio: Key, }, + UpdateOptions(RectangleOptionsUpdate), } -impl PropertyHolder for RectangleTool {} +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .on_update(|number_input: &NumberInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + +impl PropertyHolder for RectangleTool { + fn properties(&self) -> Layout { + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value)).into()), + ); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value)).into()), + )); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} impl<'a> MessageHandler> for RectangleTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true); + if let ToolMessage::Rectangle(RectangleToolMessage::UpdateOptions(action)) = message { + match action { + RectangleOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + RectangleOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + RectangleOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + RectangleOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + RectangleOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + RectangleOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } + } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + + return; + } + + self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); } fn actions(&self) -> ActionList { @@ -73,6 +170,7 @@ impl ToolTransition for RectangleTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(RectangleToolMessage::Abort.into()), + working_color_changed: Some(RectangleToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -92,7 +190,7 @@ struct RectangleToolData { impl Fsm for RectangleToolFsmState { type ToolData = RectangleToolData; - type ToolOptions = (); + type ToolOptions = RectangleToolOptions; fn transition( self, @@ -105,7 +203,7 @@ impl Fsm for RectangleToolFsmState { render_data, .. }: &mut ToolActionHandlerData, - _tool_options: &Self::ToolOptions, + tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { use RectangleToolFsmState::*; @@ -124,9 +222,16 @@ impl Fsm for RectangleToolFsmState { responses.add(DocumentMessage::StartTransaction); shape_data.path = Some(layer_path.clone()); graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); + + let fill_color = tool_options.fill.active_color(); responses.add(GraphOperationMessage::FillSet { + layer: layer_path.clone(), + fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None }, + }); + + responses.add(GraphOperationMessage::StrokeSet { layer: layer_path, - fill: Fill::solid(global_tool_data.primary_color), + stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight), }); Drawing @@ -151,6 +256,13 @@ impl Fsm for RectangleToolFsmState { Ready } + (_, WorkingColorChanged) => { + responses.add(RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 2cdc0bc9..ca32e6c0 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -8,7 +8,6 @@ use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::assist_widgets::{PivotAssist, PivotPosition}; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton}; use crate::messages::layout::utility_types::widgets::input_widgets::{DropdownEntryData, DropdownInput}; -use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::prelude::*; @@ -137,7 +136,7 @@ impl PropertyHolder for SelectTool { PivotAssist::new(self.tool_data.pivot.to_pivot_position()) .on_update(|pivot_assist: &PivotAssist| SelectToolMessage::SetPivot { position: pivot_assist.position }.into()) .widget_holder(), - Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder(), + WidgetHolder::section_separator(), IconButton::new("AlignLeft", 24) .tooltip("Align Left") .on_update(|_| { @@ -201,7 +200,7 @@ impl PropertyHolder for SelectTool { .widget_holder(), WidgetHolder::related_separator(), PopoverButton::new("Align", "Coming soon").widget_holder(), - Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder(), + WidgetHolder::section_separator(), IconButton::new("FlipHorizontal", 24) .tooltip("Flip Horizontal") .on_update(|_| SelectToolMessage::FlipHorizontal.into()) @@ -216,7 +215,7 @@ impl PropertyHolder for SelectTool { text: "Coming soon".into(), ..Default::default() })), - Separator::new(SeparatorDirection::Horizontal, SeparatorType::Section).widget_holder(), + WidgetHolder::section_separator(), IconButton::new("BooleanUnion", 24) .tooltip("Boolean Union (coming soon)") .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 039d025e..8fcb8a94 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -1,14 +1,17 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; -use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, NumberInput, WidgetHolder}; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; -use graphene_core::vector::style::Fill; +use graphene_core::vector::style::{Fill, Stroke}; +use graphene_core::Color; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -21,22 +24,32 @@ pub struct ShapeTool { } pub struct ShapeOptions { + line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, vertices: u32, } impl Default for ShapeOptions { fn default() -> Self { - Self { vertices: 5 } + Self { + vertices: 5, + line_weight: 5., + fill: ToolColorOptions::new_secondary(), + stroke: ToolColorOptions::new_primary(), + } } } #[remain::sorted] #[impl_message(Message, ToolMessage, Shape)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum ShapeToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages DragStart, @@ -49,9 +62,15 @@ pub enum ShapeToolMessage { } #[remain::sorted] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum ShapeOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), + LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), Vertices(u32), + WorkingColors(Option, Option), } impl ToolMetadata for ShapeTool { @@ -66,26 +85,84 @@ impl ToolMetadata for ShapeTool { } } -impl PropertyHolder for ShapeTool { - fn properties(&self) -> Layout { - let sides = NumberInput::new(Some(self.options.vertices as f64)) - .label("Sides") - .int() - .min(3.) - .max(1000.) - .mode(crate::messages::layout::utility_types::widget_prelude::NumberInputMode::Increment) - .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) - .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![sides] }])) - } +fn create_sides_widget(vertices: u32) -> WidgetHolder { + NumberInput::new(Some(vertices as f64)) + .label("Sides") + .int() + .min(3.) + .max(1000.) + .mode(crate::messages::layout::utility_types::widget_prelude::NumberInputMode::Increment) + .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) + .widget_holder() } +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + +impl PropertyHolder for ShapeTool { + fn properties(&self) -> Layout { + let mut widgets = vec![create_sides_widget(self.options.vertices)]; + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value)).into()), + )); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value)).into()), + )); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} impl<'a> MessageHandler> for ShapeTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { if let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message { match action { ShapeOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices, + ShapeOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + ShapeOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + ShapeOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + ShapeOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + ShapeOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + ShapeOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -112,6 +189,7 @@ impl ToolTransition for ShapeTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(ShapeToolMessage::Abort.into()), + working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -162,9 +240,16 @@ impl Fsm for ShapeToolFsmState { let subpath = bezier_rs::Subpath::new_regular_polygon(DVec2::ZERO, tool_options.vertices as u64, 1.); graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); + + let fill_color = tool_options.fill.active_color(); responses.add(GraphOperationMessage::FillSet { + layer: layer_path.clone(), + fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None }, + }); + + responses.add(GraphOperationMessage::StrokeSet { layer: layer_path, - fill: Fill::solid(global_tool_data.primary_color), + stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight), }); Drawing @@ -189,6 +274,13 @@ impl Fsm for ShapeToolFsmState { Ready } + (_, WorkingColorChanged) => { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 2bb7f70f..2fef8517 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -1,16 +1,20 @@ use crate::consts::DRAG_THRESHOLD; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout}; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; +use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{ColorInput, WidgetHolder}; use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::snapping::SnapManager; -use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; +use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::{LayerId, Operation}; -use graphene_core::vector::style::Stroke; +use graphene_core::vector::style::{Fill, Stroke}; +use graphene_core::Color; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -24,11 +28,17 @@ pub struct SplineTool { pub struct SplineOptions { line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, } impl Default for SplineOptions { fn default() -> Self { - Self { line_weight: 5. } + Self { + line_weight: 5., + fill: ToolColorOptions::new_none(), + stroke: ToolColorOptions::new_primary(), + } } } @@ -39,6 +49,8 @@ pub enum SplineToolMessage { // Standard messages #[remain::unsorted] Abort, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages Confirm, @@ -59,7 +71,12 @@ enum SplineToolFsmState { #[remain::sorted] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum SplineOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), } impl ToolMetadata for SplineTool { @@ -74,15 +91,38 @@ impl ToolMetadata for SplineTool { } } +fn create_weight_widget(line_weight: f64) -> WidgetHolder { + NumberInput::new(Some(line_weight)) + .unit(" px") + .label("Weight") + .min(0.) + .on_update(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) + .widget_holder() +} + impl PropertyHolder for SplineTool { fn properties(&self) -> Layout { - let weight = NumberInput::new(Some(self.options.line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .on_update(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![weight] }])) + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value)).into()), + ); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + WidgetCallback::new(|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value)).into()), + )); + widgets.push(WidgetHolder::unrelated_separator()); + widgets.push(create_weight_widget(self.options.line_weight)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -91,7 +131,29 @@ impl<'a> MessageHandler> for SplineT if let ToolMessage::Spline(SplineToolMessage::UpdateOptions(action)) = message { match action { SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + SplineOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + SplineOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + SplineOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + SplineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + SplineOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.stroke.primary_working_color = primary; + self.options.stroke.secondary_working_color = secondary; + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -123,6 +185,7 @@ impl ToolTransition for SplineTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(SplineToolMessage::Abort.into()), + working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -178,7 +241,7 @@ impl Fsm for SplineToolFsmState { tool_data.weight = tool_options.line_weight; - add_spline(tool_data, global_tool_data, true, responses); + add_spline(tool_data, true, tool_options.fill.active_color(), tool_options.stroke.active_color(), responses); Drawing } @@ -194,7 +257,7 @@ impl Fsm for SplineToolFsmState { } responses.add(remove_preview(tool_data)); - add_spline(tool_data, global_tool_data, true, responses); + add_spline(tool_data, true, tool_options.fill.active_color(), tool_options.stroke.active_color(), responses); Drawing } @@ -204,14 +267,14 @@ impl Fsm for SplineToolFsmState { tool_data.next_point = pos; responses.add(remove_preview(tool_data)); - add_spline(tool_data, global_tool_data, true, responses); + add_spline(tool_data, true, tool_options.fill.active_color(), tool_options.stroke.active_color(), responses); Drawing } (Drawing, Confirm) | (Drawing, Abort) => { if tool_data.points.len() >= 2 { responses.add(remove_preview(tool_data)); - add_spline(tool_data, global_tool_data, false, responses); + add_spline(tool_data, false, tool_options.fill.active_color(), tool_options.stroke.active_color(), responses); responses.add(DocumentMessage::CommitTransaction); } else { responses.add(DocumentMessage::AbortTransaction); @@ -223,6 +286,13 @@ impl Fsm for SplineToolFsmState { Ready } + (_, WorkingColorChanged) => { + responses.add(SplineToolMessage::UpdateOptions(SplineOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { @@ -254,7 +324,7 @@ fn remove_preview(tool_data: &SplineToolData) -> Message { .into() } -fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, show_preview: bool, responses: &mut VecDeque) { +fn add_spline(tool_data: &SplineToolData, show_preview: bool, fill_color: Option, stroke_color: Option, responses: &mut VecDeque) { let mut points = tool_data.points.clone(); if show_preview { points.push(tool_data.next_point) @@ -267,8 +337,13 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses); graph_modification_utils::set_manipulator_mirror_angle(&manipulator_groups, &layer_path, true, responses); + responses.add(GraphOperationMessage::FillSet { + layer: layer_path.clone(), + fill: if fill_color.is_some() { Fill::Solid(fill_color.unwrap()) } else { Fill::None }, + }); + responses.add(GraphOperationMessage::StrokeSet { layer: layer_path.clone(), - stroke: Stroke::new(Some(global_tool_data.primary_color), tool_data.weight), + stroke: Stroke::new(stroke_color, tool_data.weight), }); } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 92e218ed..d4738e11 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -4,9 +4,10 @@ use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; -use crate::messages::layout::utility_types::widgets::input_widgets::{FontInput, NumberInput}; +use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput}; use crate::messages::portfolio::document::node_graph::new_text_network; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; @@ -34,6 +35,7 @@ pub struct TextOptions { font_size: u32, font_name: String, font_style: String, + fill: ToolColorOptions, } impl Default for TextOptions { @@ -42,20 +44,22 @@ impl Default for TextOptions { font_size: 24, font_name: "Merriweather".into(), font_style: "Normal (400)".into(), + fill: ToolColorOptions::new_primary(), } } } #[remain::sorted] #[impl_message(Message, ToolMessage, Text)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum TextToolMessage { // Standard messages #[remain::unsorted] Abort, - #[remain::unsorted] DocumentIsDirty, + #[remain::unsorted] + WorkingColorChanged, // Tool-specific messages CommitText, @@ -71,10 +75,13 @@ pub enum TextToolMessage { } #[remain::sorted] -#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum TextOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), Font { family: String, style: String }, FontSize(u32), + WorkingColors(Option, Option), } impl ToolMetadata for TextTool { @@ -89,46 +96,60 @@ impl ToolMetadata for TextTool { } } +fn create_text_widgets(tool: &TextTool) -> Vec { + let font = FontInput { + is_style_picker: false, + font_family: tool.options.font_name.clone(), + font_style: tool.options.font_style.clone(), + on_update: WidgetCallback::new(|font_input: &FontInput| { + TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { + family: font_input.font_family.clone(), + style: font_input.font_style.clone(), + }) + .into() + }), + ..Default::default() + } + .widget_holder(); + let style = FontInput { + is_style_picker: true, + font_family: tool.options.font_name.clone(), + font_style: tool.options.font_style.clone(), + on_update: WidgetCallback::new(|font_input: &FontInput| { + TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { + family: font_input.font_family.clone(), + style: font_input.font_style.clone(), + }) + .into() + }), + ..Default::default() + } + .widget_holder(); + let size = NumberInput::new(Some(tool.options.font_size as f64)) + .unit(" px") + .label("Size") + .int() + .min(1.) + .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()) + .widget_holder(); + vec![font, WidgetHolder::related_separator(), style, WidgetHolder::related_separator(), size] +} + impl PropertyHolder for TextTool { fn properties(&self) -> Layout { - let font = FontInput { - is_style_picker: false, - font_family: self.options.font_name.clone(), - font_style: self.options.font_style.clone(), - on_update: WidgetCallback::new(|font_input: &FontInput| { - TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { - family: font_input.font_family.clone(), - style: font_input.font_style.clone(), - }) - .into() - }), - ..Default::default() - } - .widget_holder(); - let style = FontInput { - is_style_picker: true, - font_family: self.options.font_name.clone(), - font_style: self.options.font_style.clone(), - on_update: WidgetCallback::new(|font_input: &FontInput| { - TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { - family: font_input.font_family.clone(), - style: font_input.font_style.clone(), - }) - .into() - }), - ..Default::default() - } - .widget_holder(); - let size = NumberInput::new(Some(self.options.font_size as f64)) - .unit(" px") - .label("Size") - .int() - .min(1.) - .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()) - .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { - widgets: vec![font, WidgetHolder::related_separator(), style, WidgetHolder::related_separator(), size], - }])) + let mut widgets = create_text_widgets(self); + + widgets.push(WidgetHolder::section_separator()); + + widgets.append(&mut self.options.fill.create_widgets( + "Fill", + true, + WidgetCallback::new(|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into()), + |color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()), + WidgetCallback::new(|color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value)).into()), + )); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -143,7 +164,22 @@ impl<'a> MessageHandler> for TextToo self.register_properties(responses, LayoutTarget::ToolOptions); } TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size, + TextOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + TextOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + TextOptionsUpdate::WorkingColors(primary, secondary) => { + self.options.fill.primary_working_color = primary; + self.options.fill.secondary_working_color = secondary; + } } + + responses.add(LayoutMessage::SendLayout { + layout: self.properties(), + layout_target: LayoutTarget::ToolOptions, + }); + return; } @@ -172,6 +208,7 @@ impl ToolTransition for TextTool { document_dirty: Some(TextToolMessage::DocumentIsDirty.into()), tool_abort: Some(TextToolMessage::Abort.into()), selection_changed: Some(TextToolMessage::DocumentIsDirty.into()), + working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()), ..Default::default() } } @@ -188,7 +225,7 @@ pub struct EditingText { text: String, font: Font, font_size: f64, - color: Color, + color: Option, transform: DAffine2, } @@ -211,7 +248,7 @@ impl TextToolData { text: editing_text.text.clone(), line_width: None, font_size: editing_text.font_size, - color: editing_text.color, + color: editing_text.color.unwrap_or(Color::BLACK), url: render_data.font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(), transform: editing_text.transform.to_cols_array(), }); @@ -237,7 +274,7 @@ impl TextToolData { text: text.clone(), font: font.clone(), font_size, - color, + color: Some(color), transform, }); self.new_text = text.clone(); @@ -291,7 +328,7 @@ impl TextToolData { }); responses.add(GraphOperationMessage::FillSet { layer: self.layer_path.clone(), - fill: Fill::solid(editing_text.color), + fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None }, }); responses.add(GraphOperationMessage::TransformSet { layer: self.layer_path.clone(), @@ -470,7 +507,7 @@ impl Fsm for TextToolFsmState { transform: DAffine2::from_translation(input.mouse.position), font_size: tool_options.font_size as f64, font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()), - color: global_tool_data.primary_color, + color: tool_options.fill.active_color(), }); tool_data.new_text = String::new(); tool_data.layer_path = document.get_path_for_new_layer(); @@ -521,6 +558,13 @@ impl Fsm for TextToolFsmState { tool_data.update_bounds_overlay(document, render_data, responses); TextToolFsmState::Editing } + (_, TextToolMessage::WorkingColorChanged) => { + responses.add(TextToolMessage::UpdateOptions(TextOptionsUpdate::WorkingColors( + Some(global_tool_data.primary_color), + Some(global_tool_data.secondary_color), + ))); + self + } _ => self, } } else { diff --git a/frontend/assets/icon-16px-solid/custom-color.svg b/frontend/assets/icon-16px-solid/custom-color.svg new file mode 100644 index 00000000..949bfdae --- /dev/null +++ b/frontend/assets/icon-16px-solid/custom-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/widgets/inputs/ColorInput.svelte b/frontend/src/components/widgets/inputs/ColorInput.svelte index fd347174..3758e18e 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend/src/components/widgets/inputs/ColorInput.svelte @@ -15,8 +15,8 @@ export let value: Color; // TODO: Implement // export let allowTransparency = false; - // export let allowNone = false; // export let disabled = false; + export let allowNone = false; export let tooltip: string | undefined = undefined; export let sharpRightCorners = false; @@ -45,7 +45,7 @@ value = detail; dispatch("value", detail); }} - allowNone={true} + {allowNone} /> diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index f577450c..12c9fc3f 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -95,6 +95,7 @@ import BooleanUnion from "@graphite-frontend/assets/icon-16px-solid/boolean-unio import CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg"; import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.svg"; import Copy from "@graphite-frontend/assets/icon-16px-solid/copy.svg"; +import CustomColor from "@graphite-frontend/assets/icon-16px-solid/custom-color.svg"; import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg"; import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.svg"; import EyeHidden from "@graphite-frontend/assets/icon-16px-solid/eye-hidden.svg"; @@ -155,6 +156,7 @@ const SOLID_16PX = { CheckboxChecked: { svg: CheckboxChecked, size: 16 }, CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 }, Copy: { svg: Copy, size: 16 }, + CustomColor: { svg: CustomColor, size: 16 }, Edit: { svg: Edit, size: 16 }, Eyedropper: { svg: Eyedropper, size: 16 }, EyeHidden: { svg: EyeHidden, size: 16 }, diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 84579e5c..8c7b9efb 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -832,9 +832,10 @@ export class ColorInput extends WidgetProps { ) value!: Color; + allowNone!: boolean; + // TODO: Implement // allowTransparency!: boolean; - // allowNone!: boolean; // disabled!: boolean; @Transform(({ value }: { value: string }) => value || undefined)