diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 90ee2d4e5a..b694f45d88 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -421,4 +421,32 @@ mod test { let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); } + + #[test] + fn check_if_graphite_file_version_upgrade_is_needed() { + init_logger(); + set_uuid_seed(0); + let mut editor = Editor::new(); + let test_file = include_str!("./graphite-test-document.graphite"); + let responses = editor.handle_message(PortfolioMessage::OpenDocumentFile { + document_name: "Graphite Version Test".into(), + document_serialized_content: test_file.into(), + }); + + for response in responses { + if let FrontendMessage::DisplayDialogError { title, description } = response { + println!(); + println!("-------------------------------------------------"); + println!("Failed test due to receiving a DisplayDialogError while loading the graphite sample file!"); + println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`"); + println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file"); + println!("DisplayDialogError details:"); + println!("Title: {}", title); + println!("description: {}", description); + println!("-------------------------------------------------"); + println!(); + panic!() + } + } + } } diff --git a/editor/src/communication/graphite-test-document.graphite b/editor/src/communication/graphite-test-document.graphite new file mode 100644 index 0000000000..bec9ef7906 --- /dev/null +++ b/editor/src/communication/graphite-test-document.graphite @@ -0,0 +1 @@ +{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":919378319526168453,"layer_ids":[919378319526168452],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[303.890625,0.0,-0.0,362.10546875],"translation":[-148.83984375,-235.8828125]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":0,"name":"Untitled Document","layer_metadata":[[[919378319526168452],{"selected":true,"expanded":false}],[[],{"selected":false,"expanded":true}]],"layer_range_selection_reference":[919378319526168452],"movement_handler":{"pan":[-118.8,-45.60000000000001],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[259.88359375,366.9]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[919378319526168452],"Artwork"]},"overlays_visible":true,"snapping_enabled":true,"view_mode":"Normal","version":"0.0.5"} \ No newline at end of file diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 458a7c748d..fc2e6f1585 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -57,5 +57,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.4"; +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.5"; pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05; diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index 678f5dc912..6c44404304 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -3,6 +3,7 @@ use crate::message_prelude::*; use graphene::color::Color; use graphene::document::Document as GrapheneDocument; use graphene::layers::style::{self, Fill, ViewMode}; +use graphene::DocumentResponse; use graphene::Operation as DocumentOperation; use glam::DAffine2; @@ -31,7 +32,18 @@ impl MessageHandler for ArtboardMessageHandler { // Sub-messages #[remain::unsorted] DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(*operation) { - Ok(_) => (), + Ok(Some(document_responses)) => { + for response in document_responses { + match &response { + DocumentResponse::LayerChanged { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: path.clone() }.into()), + DocumentResponse::DeletedLayer { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasDeleted { path: path.clone() }.into()), + DocumentResponse::DocumentChanged => responses.push_back(ArtboardMessage::RenderArtboards.into()), + _ => {} + }; + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + } + Ok(None) => {} Err(e) => log::error!("Artboard Error: {:?}", e), }, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 71cac318dc..0dc6cdb384 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,5 +1,7 @@ use super::clipboards::Clipboard; use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; +use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData; +use super::utility_types::TargetDocument; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler}; use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler}; @@ -503,7 +505,6 @@ impl DocumentMessageHandler { impl PropertyHolder for DocumentMessageHandler { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::OptionalInput(OptionalInput { checked: self.snapping_enabled, @@ -704,7 +705,14 @@ impl MessageHandler for Docum } #[remain::unsorted] PropertiesPanel(message) => { - self.properties_panel_message_handler.process_action(message, &self.graphene_document, responses); + self.properties_panel_message_handler.process_action( + message, + PropertiesPanelMessageHandlerData { + artwork_document: &self.graphene_document, + artboard_document: &self.artboard_message_handler.artboards_graphene_document, + }, + responses, + ); } // Messages @@ -721,7 +729,13 @@ impl MessageHandler for Docum if selected_paths.is_empty() { responses.push_back(PropertiesPanelMessage::ClearSelection.into()) } else { - responses.push_back(PropertiesPanelMessage::SetActiveLayers { paths: selected_paths }.into()) + responses.push_back( + PropertiesPanelMessage::SetActiveLayers { + paths: selected_paths, + document: TargetDocument::Artwork, + } + .into(), + ) } // TODO: Correctly update layer panel in clear_selection instead of here diff --git a/editor/src/document/properties_panel_message.rs b/editor/src/document/properties_panel_message.rs index dfb4301ac9..280ef641b5 100644 --- a/editor/src/document/properties_panel_message.rs +++ b/editor/src/document/properties_panel_message.rs @@ -1,5 +1,7 @@ use crate::message_prelude::*; +use super::utility_types::TargetDocument; + use graphene::layers::style::{Fill, Stroke}; use serde::{Deserialize, Serialize}; @@ -15,10 +17,10 @@ pub enum PropertiesPanelMessage { ModifyStroke { stroke: Stroke }, ModifyTransform { value: f64, transform_op: TransformOp }, ResendActiveProperties, - SetActiveLayers { paths: Vec> }, + SetActiveLayers { paths: Vec>, document: TargetDocument }, } -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)] pub enum TransformOp { X, Y, diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index 91f5a5baba..f0d2f0c030 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -1,4 +1,5 @@ use super::layer_panel::LayerDataTypeDiscriminant; +use super::utility_types::TargetDocument; use crate::document::properties_panel_message::TransformOp; use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::{ @@ -85,34 +86,50 @@ impl DAffine2Utils for DAffine2 { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct PropertiesPanelMessageHandler { - active_path: Option>, + active_selection: Option<(Vec, TargetDocument)>, } impl PropertiesPanelMessageHandler { fn matches_selected(&self, path: &[LayerId]) -> bool { - let last_active_path = self.active_path.as_ref().and_then(|v| v.last().copied()); + let last_active_path_id = self.active_selection.as_ref().and_then(|(v, _)| v.last().copied()); let last_modified = path.last().copied(); - matches!((last_active_path, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last) + matches!((last_active_path_id, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last) } + + fn create_document_operation(&self, operation: Operation) -> Message { + let (_, target_document) = self.active_selection.as_ref().unwrap(); + match *target_document { + TargetDocument::Artboard => ArtboardMessage::DispatchOperation(Box::new(operation)).into(), + TargetDocument::Artwork => DocumentMessage::DispatchOperation(Box::new(operation)).into(), + } + } +} + +pub struct PropertiesPanelMessageHandlerData<'a> { + pub artwork_document: &'a GrapheneDocument, + pub artboard_document: &'a GrapheneDocument, } -impl MessageHandler for PropertiesPanelMessageHandler { +impl<'a> MessageHandler> for PropertiesPanelMessageHandler { #[remain::check] - fn process_action(&mut self, message: PropertiesPanelMessage, data: &GrapheneDocument, responses: &mut VecDeque) { - let graphene_document = data; + fn process_action(&mut self, message: PropertiesPanelMessage, data: PropertiesPanelMessageHandlerData, responses: &mut VecDeque) { + let PropertiesPanelMessageHandlerData { artwork_document, artboard_document } = data; + let get_document = |document_selector: TargetDocument| match document_selector { + TargetDocument::Artboard => artboard_document, + TargetDocument::Artwork => artwork_document, + }; use PropertiesPanelMessage::*; match message { - SetActiveLayers { paths } => { - if paths.len() > 1 { + SetActiveLayers { paths, document } => { + if paths.len() != 1 { // TODO: Allow for multiple selected layers responses.push_back(PropertiesPanelMessage::ClearSelection.into()) } else { let path = paths.into_iter().next().unwrap(); - let layer = graphene_document.layer(&path).unwrap(); - register_layer_properties(layer, responses); - self.active_path = Some(path) + self.active_selection = Some((path, document)); + responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()) } } ClearSelection => { @@ -132,8 +149,8 @@ impl MessageHandler for PropertiesPan ); } ModifyTransform { value, transform_op } => { - let path = self.active_path.as_ref().expect("Received update for properties panel with no active layer"); - let layer = graphene_document.layer(path).unwrap(); + let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer"); + let layer = get_document(*target_document).layer(path).unwrap(); use TransformOp::*; let action = match transform_op { @@ -150,35 +167,31 @@ impl MessageHandler for PropertiesPan _ => 1., }; - responses.push_back( - Operation::SetLayerTransform { - path: path.clone(), - transform: action(layer.transform, value / scale).to_cols_array(), - } - .into(), - ); + responses.push_back(self.create_document_operation(Operation::SetLayerTransform { + path: path.clone(), + transform: action(layer.transform, value / scale).to_cols_array(), + })); } ModifyName { name } => { - 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()) + let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name })) } 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()); + let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill })); } ModifyStroke { stroke } => { - let path = self.active_path.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::SetLayerStroke { path, stroke }.into()) + let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(self.create_document_operation(Operation::SetLayerStroke { path, stroke })) } CheckSelectedWasUpdated { path } => { if self.matches_selected(&path) { - let layer = graphene_document.layer(&path).unwrap(); - register_layer_properties(layer, responses); + responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()) } } CheckSelectedWasDeleted { path } => { if self.matches_selected(&path) { - self.active_path = None; + self.active_selection = None; responses.push_back( LayoutMessage::SendLayout { layout_target: LayoutTarget::PropertiesOptionsPanel, @@ -196,9 +209,12 @@ 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) + let (path, target_document) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); + let layer = get_document(target_document).layer(&path).unwrap(); + match target_document { + TargetDocument::Artboard => register_artboard_layer_properties(layer, responses), + TargetDocument::Artwork => register_artwork_layer_properties(layer, responses), + } } } } @@ -208,9 +224,189 @@ impl MessageHandler for PropertiesPan } } -fn register_layer_properties(layer: &Layer, responses: &mut VecDeque) { +fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque) { + let options_bar = vec![LayoutRow::Row { + widgets: vec![ + WidgetHolder::new(Widget::IconLabel(IconLabel { + icon: "NodeArtboard".into(), + gap_after: true, + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Artboard".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextInput(TextInput { + value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()), + on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Options Bar".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + ], + }]; + + let properties_body = { + let shape = if let LayerDataType::Shape(shape) = &layer.data { + shape + } else { + panic!("Artboards can only be shapes") + }; + let color = if let Fill::Solid(color) = shape.style.fill() { + color + } else { + panic!("Artboard must have a solid fill") + }; + + vec![LayoutRow::Section { + name: "Artboard".into(), + layout: vec![ + LayoutRow::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Location".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.x(), + label: "X".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input: &NumberInput| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::X, + } + .into() + }), + ..NumberInput::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.transform.y(), + label: "Y".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input: &NumberInput| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Y, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + LayoutRow::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Dimensions".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.bounding_transform().scale_x(), + label: "W".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input: &NumberInput| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Width, + } + .into() + }), + ..NumberInput::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: layer.bounding_transform().scale_y(), + label: "H".into(), + unit: " px".into(), + on_update: WidgetCallback::new(|number_input: &NumberInput| { + PropertiesPanelMessage::ModifyTransform { + value: number_input.value, + transform_op: TransformOp::Height, + } + .into() + }), + ..NumberInput::default() + })), + ], + }, + LayoutRow::Row { + widgets: vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Background".into(), + ..TextLabel::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::ColorInput(ColorInput { + value: Some(color.rgba_hex()), + on_update: WidgetCallback::new(|text_input: &ColorInput| { + if let Some(value) = &text_input.value { + if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) { + let new_fill = Fill::Solid(color); + PropertiesPanelMessage::ModifyFill { fill: new_fill }.into() + } else { + PropertiesPanelMessage::ResendActiveProperties.into() + } + } else { + PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into() + } + }), + can_set_transparent: false, + })), + ], + }, + ], + }] + }; + + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(options_bar), + layout_target: LayoutTarget::PropertiesOptionsPanel, + } + .into(), + ); + responses.push_back( + LayoutMessage::SendLayout { + layout: WidgetLayout::new(properties_body), + layout_target: LayoutTarget::PropertiesSectionsPanel, + } + .into(), + ); +} + +fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque) { let options_bar = vec![LayoutRow::Row { - name: "".into(), widgets: vec![ match &layer.data { LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { @@ -301,7 +497,6 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { name: "Transform".into(), layout: vec![ LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Location".into(), @@ -344,7 +539,6 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Rotation".into(), @@ -370,7 +564,6 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Scale".into(), @@ -413,7 +606,6 @@ fn node_section_transform(layer: &Layer) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Dimensions".into(), @@ -464,7 +656,6 @@ fn node_section_fill(fill: &Fill) -> Option { Fill::Solid(_) | Fill::None => Some(LayoutRow::Section { name: "Fill".into(), layout: vec![LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Color".into(), @@ -488,6 +679,7 @@ fn node_section_fill(fill: &Fill) -> Option { PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into() } }), + ..ColorInput::default() })), ], }], @@ -499,7 +691,6 @@ fn node_section_fill(fill: &Fill) -> Option { name: "Fill".into(), layout: vec![ LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Gradient: 0%".into(), @@ -532,11 +723,11 @@ fn node_section_fill(fill: &Fill) -> Option { .into() } }), + ..ColorInput::default() })), ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Gradient: 100%".into(), @@ -569,6 +760,7 @@ fn node_section_fill(fill: &Fill) -> Option { .into() } }), + ..ColorInput::default() })), ], }, @@ -596,7 +788,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { name: "Stroke".into(), layout: vec![ LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Color".into(), @@ -614,11 +805,11 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { .with_color(&text_input.value) .map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into()) }), + ..ColorInput::default() })), ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Weight".into(), @@ -644,7 +835,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Dash Lengths".into(), @@ -666,7 +856,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Dash Offset".into(), @@ -692,7 +881,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Line Cap".into(), @@ -740,7 +928,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { ], }, LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Line Join".into(), @@ -789,7 +976,6 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow { }, // TODO: Gray out this row when Line Join isn't set to Miter LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Miter Limit".into(), diff --git a/editor/src/document/utility_types.rs b/editor/src/document/utility_types.rs index d50a191467..c9d9991e28 100644 --- a/editor/src/document/utility_types.rs +++ b/editor/src/document/utility_types.rs @@ -26,3 +26,9 @@ pub enum AlignAggregate { Center, Average, } + +#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize)] +pub enum TargetDocument { + Artboard, + Artwork, +} diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index f99ffaf1cd..d45ed7da56 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -52,7 +52,7 @@ pub type SubLayout = Vec; #[remain::sorted] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum LayoutRow { - Row { name: String, widgets: Vec }, + Row { widgets: Vec }, Section { name: String, layout: SubLayout }, } @@ -72,7 +72,7 @@ impl<'a> Iterator for WidgetIter<'a> { } match self.stack.pop() { - Some(LayoutRow::Row { name: _, widgets }) => { + Some(LayoutRow::Row { widgets }) => { self.current_slice = Some(widgets); self.next() } @@ -103,7 +103,7 @@ impl<'a> Iterator for WidgetIterMut<'a> { }; match self.stack.pop() { - Some(LayoutRow::Row { name: _, widgets }) => { + Some(LayoutRow::Row { widgets }) => { self.current_slice = Some(widgets); self.next() } @@ -207,6 +207,9 @@ pub struct ColorInput { #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback, + #[serde(rename = "canSetTransparent")] + #[derivative(Default(value = "true"))] + pub can_set_transparent: bool, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] diff --git a/editor/src/viewport_tools/tools/crop_tool.rs b/editor/src/viewport_tools/tools/crop_tool.rs index c98620f76f..bc87284a82 100644 --- a/editor/src/viewport_tools/tools/crop_tool.rs +++ b/editor/src/viewport_tools/tools/crop_tool.rs @@ -1,4 +1,5 @@ use crate::consts::SELECTION_TOLERANCE; +use crate::document::utility_types::TargetDocument; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; @@ -125,6 +126,13 @@ impl Fsm for CropToolFsmState { data.bounding_box_overlays = Some(bounding_box_overlays); responses.push_back(OverlaysMessage::Rerender.into()); + responses.push_back( + PropertiesPanelMessage::SetActiveLayers { + paths: vec![vec![data.selected_board.unwrap()]], + document: TargetDocument::Artboard, + } + .into(), + ); } _ => {} }; @@ -168,6 +176,14 @@ impl Fsm for CropToolFsmState { data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(intersection[0])), true, true); + responses.push_back( + PropertiesPanelMessage::SetActiveLayers { + paths: vec![intersection.clone()], + document: TargetDocument::Artboard, + } + .into(), + ); + CropToolFsmState::Dragging } else { let id = generate_uuid(); @@ -184,6 +200,8 @@ impl Fsm for CropToolFsmState { .into(), ); + responses.push_back(PropertiesPanelMessage::ClearSelection.into()); + CropToolFsmState::Drawing } } @@ -273,6 +291,16 @@ impl Fsm for CropToolFsmState { .into(), ); + // Have to put message here instead of when Artboard is created + // This might result in a few more calls but it is not reliant on the order of messages + responses.push_back( + PropertiesPanelMessage::SetActiveLayers { + paths: vec![vec![data.selected_board.unwrap()]], + document: TargetDocument::Artboard, + } + .into(), + ); + responses.push_back(ToolMessage::DocumentIsDirty.into()); CropToolFsmState::Drawing @@ -328,6 +356,15 @@ impl Fsm for CropToolFsmState { bounding_box_overlays.delete(responses); } + // Register properties when switching back to other tools + responses.push_back( + PropertiesPanelMessage::SetActiveLayers { + paths: document.selected_layers().map(|path| path.to_vec()).collect(), + document: TargetDocument::Artwork, + } + .into(), + ); + data.snap_handler.cleanup(responses); CropToolFsmState::Ready } @@ -353,6 +390,12 @@ impl Fsm for CropToolFsmState { label: String::from("Move Artboard"), plus: false, }]), + HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyBackspace])], + mouse: None, + label: String::from("Delete Artboard"), + plus: false, + }]), ]), CropToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 669958b161..5a2a2af6fe 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -60,7 +60,6 @@ enum FreehandToolFsmState { impl PropertyHolder for FreehandTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 747eef837d..f7f30375da 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -61,7 +61,6 @@ pub enum LineOptionsUpdate { impl PropertyHolder for LineTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index 9d9334f325..8a36543c8e 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -70,7 +70,6 @@ pub enum PenOptionsUpdate { impl PropertyHolder for PenTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index 26dc2f887b..3a81c91a14 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -60,7 +60,6 @@ pub enum SelectToolMessage { impl PropertyHolder for SelectTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![ WidgetHolder::new(Widget::IconButton(IconButton { icon: "AlignLeft".into(), diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 4d3954fad1..03141946b0 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -59,7 +59,6 @@ pub enum ShapeOptionsUpdate { impl PropertyHolder for ShapeTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { label: "Sides".into(), value: self.options.vertices as f64, diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index 60e7161c6e..6e367a12f4 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -64,7 +64,6 @@ pub enum SplineOptionsUpdate { impl PropertyHolder for SplineTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Weight".into(), diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index 7483fe758d..2b8e1b3f09 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -64,7 +64,6 @@ pub enum TextOptionsUpdate { impl PropertyHolder for TextTool { fn properties(&self) -> WidgetLayout { WidgetLayout::new(vec![LayoutRow::Row { - name: "".into(), widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { unit: " px".into(), label: "Font Size".into(), diff --git a/frontend/assets/16px-solid/new-layer.svg b/frontend/assets/16px-solid/new-layer.svg deleted file mode 100644 index c7f5b34d4e..0000000000 --- a/frontend/assets/16px-solid/new-layer.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/assets/16px-solid/node-artboard.svg b/frontend/assets/16px-solid/node-artboard.svg new file mode 100644 index 0000000000..21bade0860 --- /dev/null +++ b/frontend/assets/16px-solid/node-artboard.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/components/widgets/inputs/ColorInput.vue b/frontend/src/components/widgets/inputs/ColorInput.vue index f1c28b59b4..ac8bbc99e0 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.vue +++ b/frontend/src/components/widgets/inputs/ColorInput.vue @@ -1,6 +1,6 @@