diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 4a2cd9b438..85efd58cbd 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -126,8 +126,8 @@ mod test { let mut editor = create_editor_with_three_layers(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); - editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::Copy).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); @@ -159,8 +159,8 @@ mod test { let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1]; editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]])).unwrap(); - editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::Copy).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); @@ -192,7 +192,7 @@ mod test { const LINE_INDEX: usize = 0; const PEN_INDEX: usize = 1; - editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap(); + editor.handle_message(DocumentMessage::CreateFolder(vec![])).unwrap(); let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone(); let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX]; @@ -222,10 +222,10 @@ mod test { let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); - editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); + editor.handle_message(DocumentsMessage::Copy).unwrap(); editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap(); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); @@ -283,11 +283,11 @@ mod test { let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX]; editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap(); - editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap(); + editor.handle_message(DocumentsMessage::Copy).unwrap(); editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap(); editor.draw_rect(0., 800., 12., 200.); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); - editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); + editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 7f51c8ff4a..1cfe93b83a 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -1,7 +1,6 @@ pub use super::layer_panel::*; use crate::{ consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING}, - frontend::layer_panel::*, EditorError, }; use glam::{DAffine2, DVec2}; @@ -93,9 +92,9 @@ pub enum DocumentMessage { DeleteLayer(Vec), DeleteSelectedLayers, DuplicateSelectedLayers, + CreateFolder(Vec), SetBlendModeForSelectedLayers(BlendMode), SetOpacityForSelectedLayers(f64), - AddFolder(Vec), RenameLayer(Vec, String), ToggleLayerVisibility(Vec), FlipSelectedLayers(FlipAxis), @@ -103,6 +102,7 @@ pub enum DocumentMessage { FolderChanged(Vec), StartTransaction, RollbackTransaction, + GroupSelectedLayers, AbortTransaction, CommitTransaction, ExportDocument, @@ -139,7 +139,7 @@ impl DocumentMessageHandler { let _ = self.document.render_root(); self.layer_data(&path).expanded.then(|| { let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid"); - FrontendMessage::ExpandFolder { path, children }.into() + FrontendMessage::ExpandFolder { path: path.into(), children }.into() }) } @@ -154,7 +154,7 @@ impl DocumentMessageHandler { self.layer_data(path).selected = true; let data = self.layer_panel_entry(path.to_vec()).ok()?; // TODO: Add deduplication - (!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), data }.into()) + (!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into()) } pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> { @@ -165,9 +165,9 @@ impl DocumentMessageHandler { // TODO: Consider moving this to some kind of overlay manager in the future pub fn selected_layers_vector_points(&self) -> Vec { let shapes = self.selected_layers().filter_map(|path_to_shape| { - let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape.as_slice()).ok()?; + let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?; - let shape = match &self.document.layer(path_to_shape.as_slice()).ok()?.data { + let shape = match &self.document.layer(path_to_shape).ok()?.data { LayerDataType::Shape(shape) => Some(shape), LayerDataType::Folder(_) => None, }?; @@ -205,8 +205,8 @@ impl DocumentMessageHandler { self.layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true)) } - pub fn selected_layers(&self) -> impl Iterator> { - self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path)) + pub fn selected_layers(&self) -> impl Iterator { + self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())) } /// Returns the paths to all layers in order, optionally including only selected or non-selected layers. @@ -326,7 +326,7 @@ impl DocumentMessageHandler { pub fn layer_panel_entry(&mut self, path: Vec) -> Result { let data: LayerData = *layer_data(&mut self.layer_data, &path); let layer = self.document.layer(&path)?; - let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path).unwrap(), layer, path); + let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path)?, layer, path); Ok(entry) } @@ -362,7 +362,6 @@ impl MessageHandler for DocumentMessageHand match message { Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses), DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()), - AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()), StartTransaction => self.backup(), RollbackTransaction => { self.rollback().unwrap_or_else(|e| log::warn!("{}", e)); @@ -409,6 +408,32 @@ impl MessageHandler for DocumentMessageHand .into(), ) } + CreateFolder(mut path) => { + let id = generate_uuid(); + path.push(id); + self.layerdata_mut(&path).expanded = true; + responses.push_back(DocumentOperation::CreateFolder { path }.into()) + } + GroupSelectedLayers => { + let common_prefix = self.document.common_prefix(self.selected_layers()); + let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[])); + + let mut new_folder_path = common_prefix.to_vec(); + new_folder_path.push(generate_uuid()); + + responses.push_back(DocumentsMessage::Copy.into()); + responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); + responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into()); + responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into()); + responses.push_back( + DocumentsMessage::PasteIntoFolder { + path: new_folder_path.clone(), + insert_index: -1, + } + .into(), + ); + responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into()); + } SetBlendModeForSelectedLayers(blend_mode) => { self.backup(); for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { @@ -419,7 +444,7 @@ impl MessageHandler for DocumentMessageHand self.backup(); let opacity = opacity.clamp(0., 1.); - for path in self.selected_layers().cloned() { + for path in self.selected_layers().map(|path| path.to_vec()) { responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into()); } } @@ -428,7 +453,11 @@ impl MessageHandler for DocumentMessageHand } ToggleLayerExpansion(path) => { self.layer_data(&path).expanded ^= true; - responses.push_back(FolderChanged(path).into()); + match self.layer_data(&path).expanded { + true => responses.push_back(FolderChanged(path.clone()).into()), + false => responses.push_back(FrontendMessage::CollapseFolder { path: path.clone().into() }.into()), + } + responses.extend(self.layer_panel_entry(path.clone()).ok().map(|data| FrontendMessage::UpdateLayer { path: path.into(), data }.into())); } SelectionChanged => { // TODO: Hoist this duplicated code into wider system @@ -437,7 +466,7 @@ impl MessageHandler for DocumentMessageHand DeleteSelectedLayers => { self.backup(); responses.push_front(ToolMessage::SelectedLayersChanged.into()); - for path in self.selected_layers().cloned() { + for path in self.selected_layers().map(|path| path.to_vec()) { responses.push_front(DocumentOperation::DeleteLayer { path }.into()); } } @@ -469,14 +498,12 @@ impl MessageHandler for DocumentMessageHand let all_layer_paths = self .layer_data .keys() - .filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay) + .filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false)) .cloned() .collect::>(); responses.push_front(SetSelectedLayers(all_layer_paths).into()); } - DeselectAllLayers => { - responses.push_front(SetSelectedLayers(vec![]).into()); - } + DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()), DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)), DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)), Undo => { @@ -505,18 +532,19 @@ impl MessageHandler for DocumentMessageHand self.layer_data.remove(&path); Some(ToolMessage::SelectedLayersChanged.into()) } - DocumentResponse::LayerChanged { path } => (!self.document.layer(&path).unwrap().overlay).then(|| { - FrontendMessage::UpdateLayer { - path: path.clone(), - data: self.layer_panel_entry(path).unwrap(), - } - .into() + DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| { + let overlay = self.document.layer(&path).unwrap().overlay; + (!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into()) }), - DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()), + DocumentResponse::CreatedLayer { path } => { + self.layer_data.insert(path.clone(), LayerData::new(false)); + (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()) + } DocumentResponse::DocumentChanged => Some(RenderDocument.into()), }) .flatten(), ); + log::debug!("LayerPanel: {:?}", self.layer_data.keys()); } Err(e) => log::error!("DocumentError: {:?}", e), Ok(_) => (), @@ -551,7 +579,7 @@ impl MessageHandler for DocumentMessageHand NudgeSelectedLayers(x, y) => { self.backup(); - for path in self.selected_layers().cloned() { + for path in self.selected_layers().map(|path| path.to_vec()) { let operation = DocumentOperation::TransformLayerInViewport { path, transform: DAffine2::from_translation((x, y).into()).to_cols_array(), @@ -561,9 +589,9 @@ impl MessageHandler for DocumentMessageHand responses.push_back(ToolMessage::SelectedLayersChanged.into()); } MoveSelectedLayersTo { path, insert_index } => { - responses.push_back(DocumentsMessage::CopySelectedLayers.into()); + responses.push_back(DocumentsMessage::Copy.into()); responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); - responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into()); + responses.push_back(DocumentsMessage::PasteIntoFolder { path, insert_index }.into()); } ReorderSelectedLayers(relative_position) => { self.backup(); @@ -574,7 +602,11 @@ impl MessageHandler for DocumentMessageHand 1 => selected_layers.last(), _ => unreachable!(), } { - if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) { + let all_layer_paths: Vec<_> = all_layer_paths + .iter() + .filter(|layer| layer.starts_with(&pivot[0..pivot.len() - 1]) && pivot.len() == layer.len()) + .collect(); + if let Some(pos) = all_layer_paths.iter().position(|path| *path == pivot) { let max = all_layer_paths.len() as i64 - 1; let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize; let insert = all_layer_paths.get(insert_pos); @@ -602,13 +634,13 @@ impl MessageHandler for DocumentMessageHand FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::Y => DVec2::new(1., -1.), }; - if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) { + if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) { let center = (max + min) / 2.; let bbox_trans = DAffine2::from_translation(-center); for path in self.selected_layers() { responses.push_back( DocumentOperation::TransformLayerInScope { - path: path.clone(), + path: path.to_vec(), transform: DAffine2::from_scale(scale).to_cols_array(), scope: bbox_trans.to_cols_array(), } @@ -627,7 +659,7 @@ impl MessageHandler for DocumentMessageHand AlignAxis::Y => DVec2::Y, }; let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); - if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) { + if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) { let aggregated = match aggregate { AlignAggregate::Min => combined_box[0], AlignAggregate::Max => combined_box[1], @@ -643,7 +675,7 @@ impl MessageHandler for DocumentMessageHand let translation = (aggregated - center) * axis; responses.push_back( DocumentOperation::TransformLayerInViewport { - path: path.clone(), + path: path.to_vec(), transform: DAffine2::from_translation(translation).to_cols_array(), } .into(), @@ -673,6 +705,7 @@ impl MessageHandler for DocumentMessageHand DuplicateSelectedLayers, NudgeSelectedLayers, ReorderSelectedLayers, + GroupSelectedLayers, ); common.extend(select); } diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 9f66c85885..576ef7ae44 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,6 +1,6 @@ use crate::input::InputPreprocessor; use crate::message_prelude::*; -use graphene::layers::Layer; +use graphene::layers::{Layer, LayerDataType}; use graphene::{LayerId, Operation as DocumentOperation}; use log::warn; @@ -13,11 +13,12 @@ use crate::consts::DEFAULT_DOCUMENT_NAME; #[impl_message(Message, Documents)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum DocumentsMessage { - CopySelectedLayers, - PasteLayers { + Copy, + PasteIntoFolder { path: Vec, insert_index: isize, }, + Paste, SelectDocument(usize), CloseDocument(usize), #[child] @@ -79,7 +80,7 @@ impl DocumentsMessageHandler { responses.push_back( FrontendMessage::ExpandFolder { - path: Vec::new(), + path: Vec::new().into(), children: Vec::new(), } .into(), @@ -156,7 +157,13 @@ impl MessageHandler for DocumentsMessageHa } let lp = self.active_document_mut().layer_panel(&[]).expect("Could not get panel for active doc"); - responses.push_back(FrontendMessage::ExpandFolder { path: Vec::new(), children: lp }.into()); + responses.push_back( + FrontendMessage::ExpandFolder { + path: Vec::new().into(), + children: lp, + } + .into(), + ); responses.push_back( FrontendMessage::SetActiveDocument { document_index: self.active_document_index, @@ -211,7 +218,7 @@ impl MessageHandler for DocumentsMessageHa let id = (self.active_document_index + self.documents.len() - 1) % self.documents.len(); responses.push_back(SelectDocument(id).into()); } - CopySelectedLayers => { + Copy => { let paths = self.active_document().selected_layers_sorted(); self.copy_buffer.clear(); for path in paths { @@ -223,9 +230,24 @@ impl MessageHandler for DocumentsMessageHa } } } - PasteLayers { path, insert_index } => { + Paste => { + let document = self.active_document(); + let shallowest_common_folder = document + .document + .deepest_common_folder(document.selected_layers()) + .expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); + + responses.push_back( + PasteIntoFolder { + path: shallowest_common_folder.to_vec(), + insert_index: -1, + } + .into(), + ); + } + PasteIntoFolder { path, insert_index } => { let paste = |layer: &Layer, responses: &mut VecDeque<_>| { - log::trace!("pasting into folder {:?} as index: {}", path, insert_index); + log::trace!("Pasting into folder {:?} as index: {}", path, insert_index); responses.push_back( DocumentOperation::PasteLayer { layer: layer.clone(), @@ -255,12 +277,13 @@ impl MessageHandler for DocumentsMessageHa CloseAllDocuments, NextDocument, PrevDocument, - PasteLayers, + PasteIntoFolder, + Paste, ); if self.active_document().layer_data.values().any(|data| data.selected) { let select = actions!(DocumentsMessageDiscriminant; - CopySelectedLayers, + Copy, ); common.extend(select); } diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index adb0d22c9e..50cbab516f 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -1,11 +1,13 @@ -use crate::{consts::VIEWPORT_ROTATE_SNAP_INTERVAL, frontend::layer_panel::*}; +use crate::consts::VIEWPORT_ROTATE_SNAP_INTERVAL; use glam::{DAffine2, DVec2}; +use graphene::layers::{BlendMode, LayerDataType}; use graphene::{ layers::{Layer, LayerData as DocumentLayerData}, LayerId, }; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeSeq, Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)] pub struct LayerData { @@ -49,10 +51,7 @@ impl LayerData { } pub fn layer_data<'a>(layer_data: &'a mut HashMap, LayerData>, path: &[LayerId]) -> &'a mut LayerData { - if !layer_data.contains_key(path) { - layer_data.insert(path.to_vec(), LayerData::new(false)); - } - layer_data.get_mut(path).unwrap() + layer_data.get_mut(path).expect(&format!("Layer data cannot be found because the path {:?} does not exist", path)) } pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec) -> LayerPanelEntry { @@ -78,9 +77,6 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La String::new() }; - // LayerIds are sent as (u32, u32) because jsond does not support u64s - let path = path.iter().map(|id| ((id >> 32) as u32, (id << 32 >> 32) as u32)).collect::>(); - LayerPanelEntry { name, visible: layer.visible, @@ -88,7 +84,74 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La opacity: layer.opacity, layer_type: (&layer.data).into(), layer_data: *layer_data, - path, + path: path.into(), thumbnail, } } + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct Path(Vec); + +impl From> for Path { + fn from(iter: Vec) -> Self { + Self(iter) + } +} +impl Serialize for Path { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for e in self.0.iter() { + #[cfg(target_arch = "wasm32")] + { + // LayerIds are sent as (u32, u32) because json does not support u64s + let id = ((e >> 32) as u32, (e << 32 >> 32) as u32); + seq.serialize_element(&id)?; + } + #[cfg(not(target_arch = "wasm32"))] + seq.serialize_element(e)?; + } + seq.end() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct LayerPanelEntry { + pub name: String, + pub visible: bool, + pub blend_mode: BlendMode, + pub opacity: f64, + pub layer_type: LayerType, + pub layer_data: LayerData, + pub path: crate::document::layer_panel::Path, + pub thumbnail: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum LayerType { + Folder, + Shape, +} + +impl fmt::Display for LayerType { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let name = match self { + LayerType::Folder => "Folder", + LayerType::Shape => "Shape", + }; + + formatter.write_str(name) + } +} + +impl From<&LayerDataType> for LayerType { + fn from(data: &LayerDataType) -> Self { + use LayerDataType::*; + match data { + Folder(_) => LayerType::Folder, + Shape(_) => LayerType::Shape, + } + } +} diff --git a/editor/src/document/mod.rs b/editor/src/document/mod.rs index aa24342026..b04e348309 100644 --- a/editor/src/document/mod.rs +++ b/editor/src/document/mod.rs @@ -1,6 +1,6 @@ mod document_file; mod document_message_handler; -mod layer_panel; +pub mod layer_panel; mod movement_handler; #[doc(inline)] diff --git a/editor/src/frontend/frontend_message_handler.rs b/editor/src/frontend/frontend_message_handler.rs index fddce953bd..faadf950ca 100644 --- a/editor/src/frontend/frontend_message_handler.rs +++ b/editor/src/frontend/frontend_message_handler.rs @@ -1,4 +1,4 @@ -use crate::frontend::layer_panel::LayerPanelEntry; +use crate::document::layer_panel::{LayerPanelEntry, Path}; use crate::message_prelude::*; use crate::tool::tool_options::ToolOptions; use crate::Color; @@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize}; #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] pub enum FrontendMessage { - CollapseFolder { path: Vec }, - ExpandFolder { path: Vec, children: Vec }, + CollapseFolder { path: Path }, + ExpandFolder { path: Path, children: Vec }, SetActiveTool { tool_name: String, tool_options: Option }, SetActiveDocument { document_index: usize }, UpdateOpenDocumentsList { open_documents: Vec }, @@ -17,7 +17,7 @@ pub enum FrontendMessage { DisplayConfirmationToCloseAllDocuments, UpdateCanvas { document: String }, UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) }, - UpdateLayer { path: Vec, data: LayerPanelEntry }, + UpdateLayer { path: Path, data: LayerPanelEntry }, ExportDocument { document: String, name: String }, SaveDocument { document: String, name: String }, OpenDocumentBrowse, diff --git a/editor/src/frontend/layer_panel.rs b/editor/src/frontend/layer_panel.rs deleted file mode 100644 index 0552f8a8ca..0000000000 --- a/editor/src/frontend/layer_panel.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::document::LayerData; -use graphene::layers::{BlendMode, LayerDataType}; -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct LayerPanelEntry { - pub name: String, - pub visible: bool, - pub blend_mode: BlendMode, - pub opacity: f64, - pub layer_type: LayerType, - pub layer_data: LayerData, - // TODO: Instead of turning the u64 into (u32, u32)s here, do that in the WASM translation layer - pub path: Vec<(u32, u32)>, - pub thumbnail: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum LayerType { - Folder, - Shape, -} - -impl fmt::Display for LayerType { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - let name = match self { - LayerType::Folder => "Folder", - LayerType::Shape => "Shape", - }; - - formatter.write_str(name) - } -} - -impl From<&LayerDataType> for LayerType { - fn from(data: &LayerDataType) -> Self { - use LayerDataType::*; - match data { - Folder(_) => LayerType::Folder, - Shape(_) => LayerType::Shape, - } - } -} diff --git a/editor/src/frontend/mod.rs b/editor/src/frontend/mod.rs index 3f2d6b3cd0..409a4ef4c0 100644 --- a/editor/src/frontend/mod.rs +++ b/editor/src/frontend/mod.rs @@ -1,4 +1,3 @@ pub mod frontend_message_handler; -pub mod layer_panel; pub use frontend_message_handler::{FrontendMessage, FrontendMessageDiscriminant}; diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index dcc174b035..3b7e697626 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -126,10 +126,11 @@ macro_rules! mapping { impl Default for Mapping { fn default() -> Self { use Key::*; + // WARNING! + // If a new mapping isn't being handled (and perhaps another lower-precedence one is instead), make sure to advertise + // it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`) let mappings = mapping![ - entry! {action=DocumentsMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]}, - entry! {action=MovementMessage::EnableSnapping, key_down=KeyShift}, - entry! {action=MovementMessage::DisableSnapping, key_up=KeyShift}, + // Higher priority than entries in sections below // Select entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove}, entry! {action=SelectMessage::DragStart{add_to_selection: KeyShift}, key_down=Lmb}, @@ -188,10 +189,12 @@ impl Default for Mapping { // Editor Actions entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]}, // Document Actions + entry! {action=DocumentsMessage::Paste, key_down=KeyV, modifiers=[KeyControl]}, entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]}, entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]}, + entry! {action=DocumentMessage::CreateFolder(vec![]), key_down=KeyN, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete}, entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX}, entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace}, @@ -225,7 +228,8 @@ impl Default for Mapping { entry! {action=DocumentsMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]}, entry! {action=DocumentsMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]}, entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]}, - entry! {action=DocumentsMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]}, + entry! {action=DocumentsMessage::Copy, key_down=KeyC, modifiers=[KeyControl]}, + entry! {action=DocumentMessage::GroupSelectedLayers, key_down=KeyG}, // Nudging entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]}, entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]}, diff --git a/editor/src/tool/mod.rs b/editor/src/tool/mod.rs index 60d72081ac..17c31c947f 100644 --- a/editor/src/tool/mod.rs +++ b/editor/src/tool/mod.rs @@ -190,7 +190,7 @@ impl ToolType { ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New }, ToolType::Pen => ToolOptions::Pen { weight: 5 }, ToolType::Line => ToolOptions::Line { weight: 5 }, - ToolType::Ellipse => ToolOptions::Ellipse, + ToolType::Ellipse => ToolOptions::Ellipse {}, ToolType::Shape => ToolOptions::Shape { shape_type: ShapeType::Polygon { vertices: 6 }, }, diff --git a/editor/src/tool/tool_options.rs b/editor/src/tool/tool_options.rs index 03da9a0d16..f4a586175b 100644 --- a/editor/src/tool/tool_options.rs +++ b/editor/src/tool/tool_options.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] pub enum ToolOptions { Select { append_mode: SelectAppendMode }, - Ellipse, + Ellipse {}, Shape { shape_type: ShapeType }, Line { weight: u32 }, Pen { weight: u32 }, diff --git a/editor/src/tool/tools/path.rs b/editor/src/tool/tools/path.rs index a5999352d3..70ed995656 100644 --- a/editor/src/tool/tools/path.rs +++ b/editor/src/tool/tools/path.rs @@ -115,6 +115,7 @@ impl Fsm for PathToolFsmState { shape_i += 1; for segment in &shape_to_draw.segments { + // TODO: We draw each anchor point twice because segment has it on both ends, fix this let (anchors, handles, anchor_handle_lines) = match segment { VectorManipulatorSegment::Line(a1, a2) => (vec![*a1, *a2], vec![], vec![]), VectorManipulatorSegment::Quad(a1, h1, a2) => (vec![*a1, *a2], vec![*h1], vec![(*h1, *a1)]), diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index 94a7e51261..1d47f0a6f2 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -129,8 +129,14 @@ impl Fsm for SelectToolFsmState { (None, Some(path)) => Operation::DeleteLayer { path }.into(), (Some([pos1, pos2]), path) => { let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer)); + data.bounding_box_path = Some(path.clone()); + + let half_pixel_offset = DVec2::splat(0.5); + let pos1 = pos1 + half_pixel_offset; + let pos2 = pos2 - half_pixel_offset; let transform = transform_from_box(pos1, pos2); + Operation::SetLayerTransformInViewport { path, transform }.into() } (_, _) => Message::NoOp, @@ -143,7 +149,7 @@ impl Fsm for SelectToolFsmState { data.drag_start = input.mouse.position; data.drag_current = input.mouse.position; let mut buffer = Vec::new(); - let mut selected: Vec<_> = document.selected_layers().cloned().collect(); + let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect(); let quad = data.selection_quad(); let intersection = document.document.intersects_quad_root(quad); // If no layer is currently selected and the user clicks on a shape, select that. @@ -185,7 +191,7 @@ impl Fsm for SelectToolFsmState { } (DrawingBox, MouseMove) => { data.drag_current = input.mouse.position; - let half_pixel_offset = DVec2::new(0.5, 0.5); + let half_pixel_offset = DVec2::splat(0.5); let start = data.drag_start + half_pixel_offset; let size = data.drag_current - start + half_pixel_offset; diff --git a/frontend/assets/24px-full-color/node-type-folder.svg b/frontend/assets/24px-full-color/node-type-folder.svg new file mode 100644 index 0000000000..3cba18f4b6 --- /dev/null +++ b/frontend/assets/24px-full-color/node-type-folder.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/24px-full-color/node-type-path.svg b/frontend/assets/24px-full-color/node-type-path.svg index 83aab28be4..02492004b4 100644 --- a/frontend/assets/24px-full-color/node-type-path.svg +++ b/frontend/assets/24px-full-color/node-type-path.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 73e492cdbb..836356827f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -59,6 +59,9 @@ --color-accent-hover-rgb: 73, 165, 226; --color-accent-disabled: #416277; --color-accent-disabled-rgb: 65, 98, 119; + + --color-data-raster: #e4bb72; + --color-data-raster-rgb: 228, 187, 114; } html, diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index ccda8e4f74..5d2d71c547 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -25,9 +25,17 @@ :title="layer.visible ? 'Visible' : 'Hidden'" /> + +
- + +
{{ layer.name }}
+ @@ -72,42 +82,96 @@ display: flex; height: 36px; align-items: center; - margin: 0 8px; flex: 0 0 auto; + position: relative; + + & + .layer-row { + margin-top: 2px; + } + + .layer-visibility { + flex: 0 0 auto; + margin-left: 4px; + } + + .node-connector { + flex: 0 0 auto; + width: 12px; + height: 12px; + margin: 0 2px; + border-radius: 50%; + background: var(--color-data-raster); + outline: none; + border: none; + position: relative; + + &::after { + content: ""; + position: absolute; + width: 0; + height: 0; + top: 2px; + left: 3px; + border-style: solid; + border-width: 0 3px 6px 3px; + border-color: transparent transparent var(--color-2-mildblack) transparent; + } + + &.expanded::after { + top: 3px; + left: 4px; + border-width: 3px 0 3px 6px; + border-color: transparent transparent transparent var(--color-2-mildblack); + } + } + + .node-connector-missing { + width: 16px; + flex: 0 0 auto; + } .layer { display: flex; align-items: center; + border-radius: 2px; background: var(--color-5-dullgray); - border-radius: 4px; + margin-right: 16px; width: 100%; height: 100%; - margin-left: 4px; - padding-left: 16px; - } - .selected { - background: var(--color-accent); - color: var(--color-f-white); - } + z-index: 1; - & + .layer-row { - margin-top: 2px; - } + &.selected { + background: var(--color-7-middlegray); + color: var(--color-f-white); + } - .layer-thumbnail { - width: 64px; - height: 100%; - background: white; + .layer-thumbnail { + width: 64px; + height: 100%; + background: white; + border-radius: 2px; + + svg { + width: calc(100% - 4px); + height: calc(100% - 4px); + margin: 2px; + } + } - svg { - width: calc(100% - 4px); - height: calc(100% - 4px); - margin: 2px; + .layer-type-icon { + margin-left: 8px; + margin-right: 4px; } } - .layer-type-icon { - margin: 0 8px; + .glue { + position: absolute; + background: var(--color-data-raster); + height: 6px; + bottom: -4px; + left: 44px; + right: 16px; + z-index: 0; } } } @@ -117,7 +181,7 @@