From a73d9b58115120aed9738ad07a0ccdba6ecd7ae7 Mon Sep 17 00:00:00 2001 From: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Tue, 15 Feb 2022 09:04:11 -0800 Subject: [PATCH] Can set stroke and fill on text and shapes (#551) * Can set stroke and fill on text and shapes * resend layout on failed update * text input properly resets on bad input * support modifying gradients * can modify gradients in the properties panel * updated labels * remove heap allocation in favor of RC * removed redundent line * oops --- editor/src/consts.rs | 2 +- .../src/document/document_message_handler.rs | 13 +- .../src/document/properties_panel_message.rs | 4 + .../properties_panel_message_handler.rs | 211 +++++++++++++++++- editor/src/layout/widgets.rs | 10 +- .../src/viewport_tools/tools/freehand_tool.rs | 2 +- editor/src/viewport_tools/tools/line_tool.rs | 2 +- editor/src/viewport_tools/tools/pen_tool.rs | 2 +- editor/src/viewport_tools/tools/shape_tool.rs | 2 +- .../src/viewport_tools/tools/spline_tool.rs | 2 +- editor/src/viewport_tools/tools/text_tool.rs | 2 +- .../src/components/widgets/WidgetSection.vue | 2 + .../components/widgets/inputs/TextInput.vue | 3 + graphene/src/color.rs | 37 +++ graphene/src/document.rs | 7 + graphene/src/layers/style/mod.rs | 12 +- graphene/src/operation.rs | 6 +- 17 files changed, 286 insertions(+), 33 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 9a22bd36..c6e3cb6c 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -54,5 +54,5 @@ pub const FILE_EXPORT_SUFFIX: &str = ".svg"; pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.); // Document -pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.3"; +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.4"; pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05; diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 05678ceb..c419ed96 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -480,7 +480,7 @@ impl PropertyHolder for DocumentMessageHandler { checked: self.snapping_enabled, icon: "Snapping".into(), tooltip: "Snapping".into(), - on_update: WidgetCallback::new(|updated_optional_input| DocumentMessage::SetSnapping { snap: updated_optional_input.checked }.into()), + on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()), })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { title: "Snapping".into(), @@ -508,12 +508,7 @@ impl PropertyHolder for DocumentMessageHandler { checked: self.overlays_visible, icon: "Overlays".into(), tooltip: "Overlays".into(), - on_update: WidgetCallback::new(|updated_optional_input| { - DocumentMessage::SetOverlaysVisibility { - visible: updated_optional_input.checked, - } - .into() - }), + on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()), })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { title: "Overlays".into(), @@ -561,7 +556,7 @@ impl PropertyHolder for DocumentMessageHandler { unit: "°".into(), value: self.movement_handler.tilt / (std::f64::consts::PI / 180.), increment_factor: 15., - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { MovementMessage::SetCanvasRotation { angle_radians: number_input.value * (std::f64::consts::PI / 180.), } @@ -603,7 +598,7 @@ impl PropertyHolder for DocumentMessageHandler { value: self.movement_handler.zoom * 100., min: Some(0.000001), max: Some(1000000.), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { MovementMessage::SetCanvasZoom { zoom_factor: number_input.value / 100., } diff --git a/editor/src/document/properties_panel_message.rs b/editor/src/document/properties_panel_message.rs index 182da8f2..d8e9109f 100644 --- a/editor/src/document/properties_panel_message.rs +++ b/editor/src/document/properties_panel_message.rs @@ -1,5 +1,6 @@ use crate::message_prelude::*; +use graphene::layers::style::Fill; use serde::{Deserialize, Serialize}; #[remain::sorted] @@ -9,8 +10,11 @@ pub enum PropertiesPanelMessage { CheckSelectedWasDeleted { path: Vec }, CheckSelectedWasUpdated { path: Vec }, ClearSelection, + ModifyFill { fill: Fill }, ModifyName { name: String }, + ModifyStroke { color: String, weight: f64 }, ModifyTransform { value: f64, transform_op: TransformOp }, + ResendActiveProperties, SetActiveLayers { paths: Vec> }, } diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index f64fc590..21b9dc22 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -6,13 +6,16 @@ use crate::layout::widgets::{ }; use crate::message_prelude::*; +use graphene::color::Color; use graphene::document::Document as GrapheneDocument; use graphene::layers::layer_info::{Layer, LayerDataType}; +use graphene::layers::style::{Fill, Stroke}; use graphene::{LayerId, Operation}; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; use std::f64::consts::PI; +use std::rc::Rc; trait DAffine2Utils { fn width(&self) -> f64; @@ -152,6 +155,21 @@ impl MessageHandler for PropertiesPan let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); responses.push_back(DocumentMessage::SetLayerName { layer_path: path, name }.into()) } + ModifyFill { fill } => { + let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(Operation::SetLayerFill { path, fill }.into()); + } + ModifyStroke { color, weight } => { + let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); + let layer = graphene_document.layer(&path).unwrap(); + if let Some(color) = Color::from_rgba_str(&color).or(Color::from_rgb_str(&color)) { + let stroke = Stroke::new(color, weight as f32); + responses.push_back(Operation::SetLayerStroke { path, stroke }.into()) + } else { + // Failed to update, Show user unchanged state + register_layer_properties(layer, responses) + } + } CheckSelectedWasUpdated { path } => { if self.matches_selected(&path) { let layer = graphene_document.layer(&path).unwrap(); @@ -177,6 +195,11 @@ impl MessageHandler for PropertiesPan ); } } + ResendActiveProperties => { + let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); + let layer = graphene_document.layer(&path).unwrap(); + register_layer_properties(layer, responses) + } } } @@ -217,7 +240,7 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque) { })), WidgetHolder::new(Widget::TextInput(TextInput { value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), - on_update: WidgetCallback::new(|text_input| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), + on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), })), WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Related, @@ -232,13 +255,21 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque) { let properties_body = match &layer.data { LayerDataType::Folder(_) => { - vec![node_section_transform(layer)] + vec![] } - LayerDataType::Shape(_) => { - vec![node_section_transform(layer)] + LayerDataType::Shape(shape) => { + vec![ + node_section_transform(layer), + node_section_fill(&shape.style.fill()), + node_section_stroke(&shape.style.stroke().unwrap_or_default()), + ] } - LayerDataType::Text(_) => { - vec![node_section_transform(layer)] + LayerDataType::Text(text) => { + vec![ + node_section_transform(layer), + node_section_fill(&text.style.fill()), + node_section_stroke(&text.style.stroke().unwrap_or_default()), + ] } }; @@ -277,7 +308,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { value: layer.transform.x(), label: "X".into(), unit: " px".into(), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value, transform_op: TransformOp::X, @@ -294,7 +325,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { value: layer.transform.y(), label: "Y".into(), unit: " px".into(), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value, transform_op: TransformOp::Y, @@ -320,7 +351,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { value: layer.transform.width(), label: "W".into(), unit: " px".into(), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value, transform_op: TransformOp::Width, @@ -337,7 +368,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { value: layer.transform.height(), label: "H".into(), unit: " px".into(), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value, transform_op: TransformOp::Height, @@ -363,7 +394,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { value: layer.transform.rotation() * 180. / PI, label: "R".into(), unit: "°".into(), - on_update: WidgetCallback::new(|number_input| { + on_update: WidgetCallback::new(|number_input: &NumberInput| { PropertiesPanelMessage::ModifyTransform { value: number_input.value / 180. * PI, transform_op: TransformOp::Rotation, @@ -377,3 +408,161 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { ], } } + +fn node_section_fill(fill: &Fill) -> LayoutRow { + match fill { + Fill::Solid(color) => LayoutRow::Section { + name: "Fill".into(), + layout: vec![LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Color".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: color.rgba_hex(), + on_update: WidgetCallback::new(|text_input: &TextInput| { + if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) { + let new_fill = Fill::Solid(color); + PropertiesPanelMessage::ModifyFill { fill: new_fill }.into() + } else { + PropertiesPanelMessage::ResendActiveProperties.into() + } + }), + })), + ], + }], + }, + Fill::LinearGradient(gradient) => { + let gradient_1 = Rc::new(gradient.clone()); + let gradient_2 = gradient_1.clone(); + LayoutRow::Section { + name: "Fill".into(), + layout: vec![ + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Gradient: 0%".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: gradient_1.positions[0].1.rgba_hex(), + on_update: WidgetCallback::new(move |text_input: &TextInput| { + if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) { + let mut new_gradient = (*gradient_1).clone(); + new_gradient.positions[0].1 = color; + PropertiesPanelMessage::ModifyFill { + fill: Fill::LinearGradient(new_gradient), + } + .into() + } else { + PropertiesPanelMessage::ResendActiveProperties.into() + } + }), + })), + ], + }, + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Gradient: 100%".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: gradient_2.positions[1].1.rgba_hex(), + on_update: WidgetCallback::new(move |text_input: &TextInput| { + if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) { + let mut new_gradient = (*gradient_2).clone(); + new_gradient.positions[1].1 = color; + PropertiesPanelMessage::ModifyFill { + fill: Fill::LinearGradient(new_gradient), + } + .into() + } else { + PropertiesPanelMessage::ResendActiveProperties.into() + } + }), + })), + ], + }, + ], + } + } + Fill::None => panic!("`node_section_fill` called on a shape that does not have a fill"), + } +} + +fn node_section_stroke(stroke: &Stroke) -> LayoutRow { + let color = stroke.color(); + let weight = stroke.width(); + LayoutRow::Section { + name: "Stroke".into(), + layout: vec![ + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Color".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: stroke.color().rgba_hex(), + on_update: WidgetCallback::new(move |text_input: &TextInput| { + PropertiesPanelMessage::ModifyStroke { + color: text_input.value.clone(), + weight: weight as f64, + } + .into() + }), + })), + ], + }, + LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Weight".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: stroke.width() as f64, + is_integer: true, + min: Some(0.), + unit: " px".into(), + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + PropertiesPanelMessage::ModifyStroke { + color: color.rgba_hex(), + weight: number_input.value, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + ], + } +} diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index b45c5365..f0e7119f 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use super::layout_message::LayoutTarget; use crate::message_prelude::*; @@ -130,18 +132,18 @@ impl WidgetHolder { #[derive(Clone)] pub struct WidgetCallback { - pub callback: fn(&T) -> Message, + pub callback: Rc Message + 'static>, } impl WidgetCallback { - pub fn new(callback: fn(&T) -> Message) -> Self { - Self { callback } + pub fn new(callback: impl Fn(&T) -> Message + 'static) -> Self { + Self { callback: Rc::new(callback) } } } impl Default for WidgetCallback { fn default() -> Self { - Self { callback: |_| Message::NoOp } + Self::new(|_| Message::NoOp) } } diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 17db8878..669958b1 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -67,7 +67,7 @@ impl PropertyHolder for FreehandTool { value: self.options.line_weight as f64, is_integer: true, min: Some(1.), - on_update: WidgetCallback::new(|number_input| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()), ..NumberInput::default() }))], }]) diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 9873f95a..747eef83 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -68,7 +68,7 @@ impl PropertyHolder for LineTool { value: self.options.line_weight as f64, is_integer: true, min: Some(0.), - on_update: WidgetCallback::new(|number_input| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()), ..NumberInput::default() }))], }]) diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index f5cbece2..2b9a95b8 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -75,7 +75,7 @@ impl PropertyHolder for PenTool { value: self.options.line_weight as f64, is_integer: true, min: Some(0.), - on_update: WidgetCallback::new(|number_input| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()), ..NumberInput::default() }))], }]) diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 99b3c916..4d3954fa 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -66,7 +66,7 @@ impl PropertyHolder for ShapeTool { is_integer: true, min: Some(3.), max: Some(256.), - on_update: WidgetCallback::new(|number_input| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()), ..NumberInput::default() }))], }]) diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index 715955f6..60e7161c 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -71,7 +71,7 @@ impl PropertyHolder for SplineTool { value: self.options.line_weight as f64, is_integer: true, min: Some(0.), - on_update: WidgetCallback::new(|number_input| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value as u32)).into()), ..NumberInput::default() }))], }]) diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index 679b8b81..f0d08b54 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -71,7 +71,7 @@ impl PropertyHolder for TextTool { value: self.options.font_size as f64, is_integer: true, min: Some(1.), - on_update: WidgetCallback::new(|number_input| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()), ..NumberInput::default() }))], }]) diff --git a/frontend/src/components/widgets/WidgetSection.vue b/frontend/src/components/widgets/WidgetSection.vue index 7d1db9f2..9cc84b32 100644 --- a/frontend/src/components/widgets/WidgetSection.vue +++ b/frontend/src/components/widgets/WidgetSection.vue @@ -14,6 +14,8 @@