diff --git a/Cargo.lock b/Cargo.lock index ee95cae7f..a6047ccaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ dependencies = [ "kurbo", "log", "rand_chacha", + "remain", "serde", "serde_json", "spin", @@ -268,6 +269,17 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remain" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ba1e78fa68412cb93ef642fd4d20b9a941be49ee9333875ebaf13112673ea7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ryu" version = "1.0.9" diff --git a/editor/Cargo.toml b/editor/Cargo.toml index a97fa0090..80d9a7822 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -23,6 +23,7 @@ spin = "0.9.2" kurbo = { git = "https://github.com/GraphiteEditor/kurbo.git", features = [ "serde", ] } +remain = "0.2.2" [dependencies.graphene] path = "../graphene" diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index 017bc79d0..7c61acccd 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -10,11 +10,12 @@ use graphene::Operation as DocumentOperation; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +#[remain::sorted] #[impl_message(Message, DocumentMessage, Artboard)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ArtboardMessage { - DispatchOperation(Box), AddArtboard { top: f64, left: f64, height: f64, width: f64 }, + DispatchOperation(Box), RenderArtboards, } @@ -37,14 +38,12 @@ impl ArtboardMessageHandler { } impl MessageHandler for ArtboardMessageHandler { + #[remain::check] fn process_action(&mut self, message: ArtboardMessage, _data: (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor), responses: &mut VecDeque) { // let (layer_metadata, document, ipp) = data; use ArtboardMessage::*; + #[remain::sorted] match message { - DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(&operation) { - Ok(_) => (), - Err(e) => log::error!("Artboard Error: {:?}", e), - }, AddArtboard { top, left, height, width } => { let artboard_id = generate_uuid(); self.artboard_ids.push(artboard_id); @@ -64,6 +63,10 @@ impl MessageHandler match self.artboards_graphene_document.handle_operation(&operation) { + Ok(_) => (), + Err(e) => log::error!("Artboard Error: {:?}", e), + }, RenderArtboards => { // Render an infinite canvas if there are no artboards if self.artboard_ids.is_empty() { diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index e4587d21a..9cbeb13ae 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -113,68 +113,69 @@ impl Default for DocumentMessageHandler { } } +#[remain::sorted] #[impl_message(Message, PortfolioMessage, Document)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum DocumentMessage { - #[child] - Movement(MovementMessage), - #[child] - TransformLayers(TransformLayerMessage), - DispatchOperation(Box), - #[child] - Overlay(OverlayMessage), + AbortTransaction, + AddSelectedLayers(Vec>), + AlignSelectedLayers(AlignAxis, AlignAggregate), #[child] Artboard(ArtboardMessage), - UpdateLayerMetadata { - layer_path: Vec, - layer_metadata: LayerMetadata, - }, - SetSelectedLayers(Vec>), - AddSelectedLayers(Vec>), - SelectAllLayers, + CommitTransaction, + CreateEmptyFolder(Vec), DebugPrintDocument, - SelectLayer(Vec, bool, bool), - SelectionChanged, - DeselectAllLayers, DeleteLayer(Vec), DeleteSelectedLayers, - DuplicateSelectedLayers, - CreateEmptyFolder(Vec), - SetBlendModeForSelectedLayers(BlendMode), - SetOpacityForSelectedLayers(f64), - RenameLayer(Vec, String), - ToggleLayerVisibility(Vec), - FlipSelectedLayers(FlipAxis), - ToggleLayerExpansion(Vec), - SetLayerExpansion(Vec, bool), - FolderChanged(Vec), - LayerChanged(Vec), - DocumentStructureChanged, - StartTransaction, - RollbackTransaction, - GroupSelectedLayers, - UngroupSelectedLayers, - UngroupLayers(Vec), - AbortTransaction, - CommitTransaction, - ExportDocument, - SaveDocument, - RenderDocument, + DeselectAllLayers, DirtyRenderDocument, DirtyRenderDocumentInOutlineView, - SetViewMode(ViewMode), - Undo, - Redo, + DispatchOperation(Box), DocumentHistoryBackward, DocumentHistoryForward, - NudgeSelectedLayers(f64, f64), - AlignSelectedLayers(AlignAxis, AlignAggregate), + DocumentStructureChanged, + DuplicateSelectedLayers, + ExportDocument, + FlipSelectedLayers(FlipAxis), + FolderChanged(Vec), + GroupSelectedLayers, + LayerChanged(Vec), + #[child] + Movement(MovementMessage), MoveSelectedLayersTo { path: Vec, insert_index: isize, }, + NudgeSelectedLayers(f64, f64), + #[child] + Overlay(OverlayMessage), + Redo, + RenameLayer(Vec, String), + RenderDocument, ReorderSelectedLayers(i32), // relative_position, + RollbackTransaction, + SaveDocument, + SelectAllLayers, + SelectionChanged, + SelectLayer(Vec, bool, bool), + SetBlendModeForSelectedLayers(BlendMode), + SetLayerExpansion(Vec, bool), + SetOpacityForSelectedLayers(f64), + SetSelectedLayers(Vec>), SetSnapping(bool), + SetViewMode(ViewMode), + StartTransaction, + ToggleLayerExpansion(Vec), + ToggleLayerVisibility(Vec), + #[child] + TransformLayers(TransformLayerMessage), + Undo, + UngroupLayers(Vec), + UngroupSelectedLayers, + UpdateLayerMetadata { + layer_path: Vec, + layer_metadata: LayerMetadata, + }, ZoomCanvasToFitAll, } @@ -552,243 +553,15 @@ impl DocumentMessageHandler { } impl MessageHandler for DocumentMessageHandler { + #[remain::check] fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque) { use DocumentMessage::*; + #[remain::sorted] match message { - Movement(message) => self.movement_handler.process_action(message, (&self.graphene_document, ipp), responses), - TransformLayers(message) => self - .transform_layer_handler - .process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses), - DeleteLayer(path) => responses.push_front(DocumentOperation::DeleteLayer { path }.into()), - StartTransaction => self.backup(responses), - RollbackTransaction => { - self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e)); - responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); - } AbortTransaction => { self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); } - CommitTransaction => (), - Overlay(message) => { - self.overlay_message_handler.process_action( - message, - (Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp), - responses, - ); - // responses.push_back(OverlayMessage::RenderOverlays.into()); - } - Artboard(message) => { - self.artboard_message_handler.process_action( - message, - (Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp), - responses, - ); - } - ExportDocument => { - // TODO(MFISH33): Add Dialog to select artboards - let bbox = self.document_bounds().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); - let size = bbox[1] - bbox[0]; - let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { - true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX), - false => self.name.clone() + FILE_EXPORT_SUFFIX, - }; - responses.push_back( - FrontendMessage::TriggerFileDownload { - document: format!( - r#"{}{}"#, - bbox[0].x, - bbox[0].y, - size.x, - size.y, - "\n", - self.graphene_document.render_root(self.view_mode) - ), - name, - } - .into(), - ) - } - SaveDocument => { - self.set_save_state(true); - responses.push_back(PortfolioMessage::AutoSaveActiveDocument.into()); - // Update the save status of the just saved document - responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); - - let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { - true => self.name.clone(), - false => self.name.clone() + FILE_SAVE_SUFFIX, - }; - responses.push_back( - FrontendMessage::TriggerFileDownload { - document: self.serialize_document(), - name, - } - .into(), - ) - } - CreateEmptyFolder(mut path) => { - let id = generate_uuid(); - path.push(id); - responses.push_back(DocumentOperation::CreateFolder { path: path.clone() }.into()); - responses.push_back(DocumentMessage::SetLayerExpansion(path, true).into()); - } - GroupSelectedLayers => { - let mut new_folder_path: Vec = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); - - // Required for grouping parent folders with their own children - if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) { - new_folder_path.remove(new_folder_path.len() - 1); - } - - new_folder_path.push(generate_uuid()); - - responses.push_back(PortfolioMessage::Copy(Clipboard::System).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( - PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::System, - path: new_folder_path.clone(), - insert_index: -1, - } - .into(), - ); - responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into()); - } - UngroupLayers(folder_path) => { - // Select all the children of the folder - let to_select = self.graphene_document.folder_children_paths(&folder_path); - let message_buffer = [ - // Copy them - DocumentMessage::SetSelectedLayers(to_select).into(), - PortfolioMessage::Copy(Clipboard::System).into(), - // Paste them into the folder above - PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::System, - path: folder_path[..folder_path.len() - 1].to_vec(), - insert_index: -1, - } - .into(), - // Delete parent folder - DocumentMessage::DeleteLayer(folder_path).into(), - ]; - - // Push these messages in reverse due to push_front - for message in message_buffer.into_iter().rev() { - responses.push_front(message); - } - } - UngroupSelectedLayers => { - responses.push_back(DocumentMessage::StartTransaction.into()); - let folder_paths = self.graphene_document.sorted_folders_by_depth(self.selected_layers()); - for folder_path in folder_paths { - responses.push_back(DocumentMessage::UngroupLayers(folder_path.to_vec()).into()); - } - responses.push_back(DocumentMessage::CommitTransaction.into()); - } - SetBlendModeForSelectedLayers(blend_mode) => { - self.backup(responses); - for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { - responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into()); - } - } - SetOpacityForSelectedLayers(opacity) => { - self.backup(responses); - let opacity = opacity.clamp(0., 1.); - - for path in self.selected_layers().map(|path| path.to_vec()) { - responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into()); - } - } - ToggleLayerVisibility(path) => { - responses.push_back(DocumentOperation::ToggleLayerVisibility { path }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } - ToggleLayerExpansion(path) => { - self.layer_metadata_mut(&path).expanded ^= true; - responses.push_back(DocumentStructureChanged.into()); - responses.push_back(LayerChanged(path).into()) - } - SetLayerExpansion(path, is_expanded) => { - self.layer_metadata_mut(&path).expanded = is_expanded; - responses.push_back(DocumentStructureChanged.into()); - responses.push_back(LayerChanged(path).into()) - } - SelectionChanged => { - // TODO: Hoist this duplicated code into wider system - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } - DeleteSelectedLayers => { - self.backup(responses); - - for path in self.selected_layers_without_children() { - responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into()); - } - - responses.push_front(ToolMessage::DocumentIsDirty.into()); - } - SetViewMode(mode) => { - self.view_mode = mode; - responses.push_front(DocumentMessage::DirtyRenderDocument.into()); - } - DuplicateSelectedLayers => { - self.backup(responses); - for path in self.selected_layers_sorted() { - responses.push_back(DocumentOperation::DuplicateLayer { path: path.to_vec() }.into()); - } - } - SelectLayer(selected, ctrl, shift) => { - let mut paths = vec![]; - let last_selection_exists = !self.layer_range_selection_reference.is_empty(); - - // If we have shift pressed and a layer already selected then fill the range - if shift && last_selection_exists { - // Fill the selection range - self.layer_metadata - .iter() - .filter(|(target, _)| self.graphene_document.layer_is_between(target, &selected, &self.layer_range_selection_reference)) - .for_each(|(layer_path, _)| { - paths.push(layer_path.clone()); - }); - } else { - if ctrl { - // Toggle selection when holding ctrl - let layer = self.layer_metadata_mut(&selected); - layer.selected = !layer.selected; - responses.push_back(LayerChanged(selected.clone()).into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } else { - paths.push(selected.clone()); - } - - // Set our last selection reference - self.layer_range_selection_reference = selected; - } - - // Don't create messages for empty operations - if !paths.is_empty() { - // Add or set our selected layers - if ctrl { - responses.push_front(AddSelectedLayers(paths).into()); - } else { - responses.push_front(SetSelectedLayers(paths).into()); - } - } - } - UpdateLayerMetadata { layer_path: path, layer_metadata } => { - self.layer_metadata.insert(path, layer_metadata); - } - SetSelectedLayers(paths) => { - let selected = self.layer_metadata.iter_mut().filter(|(_, layer_metadata)| layer_metadata.selected); - selected.for_each(|(path, layer_metadata)| { - layer_metadata.selected = false; - responses.push_back(LayerChanged(path.clone()).into()) - }); - - responses.push_front(AddSelectedLayers(paths).into()); - } AddSelectedLayers(paths) => { for path in paths { responses.extend(self.select_layer(&path)); @@ -797,44 +570,83 @@ impl MessageHandler for DocumentMessageHand responses.push_back(FolderChanged(Vec::new()).into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); } + AlignSelectedLayers(axis, aggregate) => { + self.backup(responses); + let (paths, boxes): (Vec<_>, Vec<_>) = self + .selected_layers() + .filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b))) + .unzip(); + + let axis = match axis { + AlignAxis::X => DVec2::X, + AlignAxis::Y => DVec2::Y, + }; + let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); + if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) { + let aggregated = match aggregate { + AlignAggregate::Min => combined_box[0], + AlignAggregate::Max => combined_box[1], + AlignAggregate::Center => lerp(&combined_box), + AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(), + }; + for (path, bbox) in paths.into_iter().zip(boxes) { + let center = match aggregate { + AlignAggregate::Min => bbox[0], + AlignAggregate::Max => bbox[1], + _ => lerp(&bbox), + }; + let translation = (aggregated - center) * axis; + responses.push_back( + DocumentOperation::TransformLayerInViewport { + path: path.to_vec(), + transform: DAffine2::from_translation(translation).to_cols_array(), + } + .into(), + ); + } + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + } + Artboard(message) => { + self.artboard_message_handler.process_action( + message, + (Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp), + responses, + ); + } + CommitTransaction => (), + CreateEmptyFolder(mut path) => { + let id = generate_uuid(); + path.push(id); + responses.push_back(DocumentOperation::CreateFolder { path: path.clone() }.into()); + responses.push_back(DocumentMessage::SetLayerExpansion(path, true).into()); + } DebugPrintDocument => { log::debug!("{:#?}\n{:#?}", self.graphene_document, self.layer_metadata); } - SelectAllLayers => { - let all_layer_paths = self.all_layers(); - responses.push_front(SetSelectedLayers(all_layer_paths.map(|path| path.to_vec()).collect()).into()); + DeleteLayer(path) => responses.push_front(DocumentOperation::DeleteLayer { path }.into()), + DeleteSelectedLayers => { + self.backup(responses); + + for path in self.selected_layers_without_children() { + responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into()); + } + + responses.push_front(ToolMessage::DocumentIsDirty.into()); } DeselectAllLayers => { responses.push_front(SetSelectedLayers(vec![]).into()); self.layer_range_selection_reference.clear(); } - DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)), - DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)), - Undo => { - responses.push_back(SelectMessage::Abort.into()); - responses.push_back(DocumentHistoryBackward.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(RenderDocument.into()); - responses.push_back(FolderChanged(vec![]).into()); + DirtyRenderDocument => { + // Mark all non-overlay caches as dirty + GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {}); + + responses.push_back(DocumentMessage::RenderDocument.into()); } - Redo => { - responses.push_back(SelectMessage::Abort.into()); - responses.push_back(DocumentHistoryForward.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(RenderDocument.into()); - responses.push_back(FolderChanged(vec![]).into()); - } - FolderChanged(path) => { - let _ = self.graphene_document.render_root(self.view_mode); - responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]); - } - DocumentStructureChanged => { - let data_buffer: RawBuffer = self.serialize_root().into(); - responses.push_back(FrontendMessage::DisplayDocumentLayerTreeStructure { data_buffer }.into()) - } - LayerChanged(path) => { - if let Ok(layer_entry) = self.layer_panel_entry(path) { - responses.push_back(FrontendMessage::UpdateDocumentLayer { data: layer_entry }.into()); + DirtyRenderDocumentInOutlineView => { + if self.view_mode == ViewMode::Outline { + responses.push_front(DocumentMessage::DirtyRenderDocument.into()); } } DispatchOperation(op) => match self.graphene_document.handle_operation(&op) { @@ -864,6 +676,144 @@ impl MessageHandler for DocumentMessageHand Err(e) => log::error!("DocumentError: {:?}", e), Ok(_) => (), }, + DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)), + DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)), + DocumentStructureChanged => { + let data_buffer: RawBuffer = self.serialize_root().into(); + responses.push_back(FrontendMessage::DisplayDocumentLayerTreeStructure { data_buffer }.into()) + } + DuplicateSelectedLayers => { + self.backup(responses); + for path in self.selected_layers_sorted() { + responses.push_back(DocumentOperation::DuplicateLayer { path: path.to_vec() }.into()); + } + } + ExportDocument => { + // TODO(MFISH33): Add Dialog to select artboards + let bbox = self.document_bounds().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); + let size = bbox[1] - bbox[0]; + let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { + true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX), + false => self.name.clone() + FILE_EXPORT_SUFFIX, + }; + responses.push_back( + FrontendMessage::TriggerFileDownload { + document: format!( + r#"{}{}"#, + bbox[0].x, + bbox[0].y, + size.x, + size.y, + "\n", + self.graphene_document.render_root(self.view_mode) + ), + name, + } + .into(), + ) + } + FlipSelectedLayers(axis) => { + self.backup(responses); + let scale = match axis { + FlipAxis::X => DVec2::new(-1., 1.), + FlipAxis::Y => DVec2::new(1., -1.), + }; + if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) { + 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.to_vec(), + transform: DAffine2::from_scale(scale).to_cols_array(), + scope: bbox_trans.to_cols_array(), + } + .into(), + ); + } + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + } + FolderChanged(path) => { + let _ = self.graphene_document.render_root(self.view_mode); + responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]); + } + GroupSelectedLayers => { + let mut new_folder_path: Vec = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); + + // Required for grouping parent folders with their own children + if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) { + new_folder_path.remove(new_folder_path.len() - 1); + } + + new_folder_path.push(generate_uuid()); + + responses.push_back(PortfolioMessage::Copy(Clipboard::System).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( + PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::System, + path: new_folder_path.clone(), + insert_index: -1, + } + .into(), + ); + responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into()); + } + LayerChanged(path) => { + if let Ok(layer_entry) = self.layer_panel_entry(path) { + responses.push_back(FrontendMessage::UpdateDocumentLayer { data: layer_entry }.into()); + } + } + Movement(message) => self.movement_handler.process_action(message, (&self.graphene_document, ipp), responses), + MoveSelectedLayersTo { path, insert_index } => { + let layers = self.selected_layers().collect::>(); + + // Trying to insert into self. + if layers.iter().any(|layer| path.starts_with(layer)) { + return; + } + let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap(); + responses.push_back(PortfolioMessage::Copy(Clipboard::System).into()); + responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); + responses.push_back( + PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::System, + path, + insert_index, + } + .into(), + ); + } + NudgeSelectedLayers(x, y) => { + self.backup(responses); + 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(), + }; + responses.push_back(operation.into()); + } + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + Overlay(message) => { + self.overlay_message_handler.process_action( + message, + (Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp), + responses, + ); + // responses.push_back(OverlayMessage::RenderOverlays.into()); + } + Redo => { + responses.push_back(SelectMessage::Abort.into()); + responses.push_back(DocumentHistoryForward.into()); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + responses.push_back(RenderDocument.into()); + responses.push_back(FolderChanged(vec![]).into()); + } + RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), RenderDocument => { responses.push_back( FrontendMessage::UpdateDocumentArtwork { @@ -909,47 +859,6 @@ impl MessageHandler for DocumentMessageHand .into(), ); } - DirtyRenderDocument => { - // Mark all non-overlay caches as dirty - GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {}); - - responses.push_back(DocumentMessage::RenderDocument.into()); - } - DirtyRenderDocumentInOutlineView => { - if self.view_mode == ViewMode::Outline { - responses.push_front(DocumentMessage::DirtyRenderDocument.into()); - } - } - NudgeSelectedLayers(x, y) => { - self.backup(responses); - 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(), - }; - responses.push_back(operation.into()); - } - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } - MoveSelectedLayersTo { path, insert_index } => { - let layers = self.selected_layers().collect::>(); - - // Trying to insert into self. - if layers.iter().any(|layer| path.starts_with(layer)) { - return; - } - let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap(); - responses.push_back(PortfolioMessage::Copy(Clipboard::System).into()); - responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); - responses.push_back( - PortfolioMessage::PasteIntoFolder { - clipboard: Clipboard::System, - path, - insert_index, - } - .into(), - ); - } ReorderSelectedLayers(relative_position) => { self.backup(responses); let all_layer_paths = self.all_layers_sorted(); @@ -981,69 +890,163 @@ impl MessageHandler for DocumentMessageHand } } } - FlipSelectedLayers(axis) => { - self.backup(responses); - let scale = match axis { - FlipAxis::X => DVec2::new(-1., 1.), - FlipAxis::Y => DVec2::new(1., -1.), - }; - if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) { - 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.to_vec(), - transform: DAffine2::from_scale(scale).to_cols_array(), - scope: bbox_trans.to_cols_array(), - } - .into(), - ); - } - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } + RollbackTransaction => { + self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e)); + responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); } - AlignSelectedLayers(axis, aggregate) => { - self.backup(responses); - let (paths, boxes): (Vec<_>, Vec<_>) = self - .selected_layers() - .filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b))) - .unzip(); + SaveDocument => { + self.set_save_state(true); + responses.push_back(PortfolioMessage::AutoSaveActiveDocument.into()); + // Update the save status of the just saved document + responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); - let axis = match axis { - AlignAxis::X => DVec2::X, - AlignAxis::Y => DVec2::Y, + let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { + true => self.name.clone(), + false => self.name.clone() + FILE_SAVE_SUFFIX, }; - let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); - if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) { - let aggregated = match aggregate { - AlignAggregate::Min => combined_box[0], - AlignAggregate::Max => combined_box[1], - AlignAggregate::Center => lerp(&combined_box), - AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(), - }; - for (path, bbox) in paths.into_iter().zip(boxes) { - let center = match aggregate { - AlignAggregate::Min => bbox[0], - AlignAggregate::Max => bbox[1], - _ => lerp(&bbox), - }; - let translation = (aggregated - center) * axis; - responses.push_back( - DocumentOperation::TransformLayerInViewport { - path: path.to_vec(), - transform: DAffine2::from_translation(translation).to_cols_array(), - } - .into(), - ); + responses.push_back( + FrontendMessage::TriggerFileDownload { + document: self.serialize_document(), + name, + } + .into(), + ) + } + SelectAllLayers => { + let all_layer_paths = self.all_layers(); + responses.push_front(SetSelectedLayers(all_layer_paths.map(|path| path.to_vec()).collect()).into()); + } + SelectionChanged => { + // TODO: Hoist this duplicated code into wider system + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + SelectLayer(selected, ctrl, shift) => { + let mut paths = vec![]; + let last_selection_exists = !self.layer_range_selection_reference.is_empty(); + + // If we have shift pressed and a layer already selected then fill the range + if shift && last_selection_exists { + // Fill the selection range + self.layer_metadata + .iter() + .filter(|(target, _)| self.graphene_document.layer_is_between(target, &selected, &self.layer_range_selection_reference)) + .for_each(|(layer_path, _)| { + paths.push(layer_path.clone()); + }); + } else { + if ctrl { + // Toggle selection when holding ctrl + let layer = self.layer_metadata_mut(&selected); + layer.selected = !layer.selected; + responses.push_back(LayerChanged(selected.clone()).into()); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } else { + paths.push(selected.clone()); + } + + // Set our last selection reference + self.layer_range_selection_reference = selected; + } + + // Don't create messages for empty operations + if !paths.is_empty() { + // Add or set our selected layers + if ctrl { + responses.push_front(AddSelectedLayers(paths).into()); + } else { + responses.push_front(SetSelectedLayers(paths).into()); } - responses.push_back(ToolMessage::DocumentIsDirty.into()); } } - RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), + SetBlendModeForSelectedLayers(blend_mode) => { + self.backup(responses); + for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { + responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into()); + } + } + SetLayerExpansion(path, is_expanded) => { + self.layer_metadata_mut(&path).expanded = is_expanded; + responses.push_back(DocumentStructureChanged.into()); + responses.push_back(LayerChanged(path).into()) + } + SetOpacityForSelectedLayers(opacity) => { + self.backup(responses); + let opacity = opacity.clamp(0., 1.); + + for path in self.selected_layers().map(|path| path.to_vec()) { + responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into()); + } + } + SetSelectedLayers(paths) => { + let selected = self.layer_metadata.iter_mut().filter(|(_, layer_metadata)| layer_metadata.selected); + selected.for_each(|(path, layer_metadata)| { + layer_metadata.selected = false; + responses.push_back(LayerChanged(path.clone()).into()) + }); + + responses.push_front(AddSelectedLayers(paths).into()); + } SetSnapping(new_status) => { self.snapping_enabled = new_status; } + SetViewMode(mode) => { + self.view_mode = mode; + responses.push_front(DocumentMessage::DirtyRenderDocument.into()); + } + StartTransaction => self.backup(responses), + ToggleLayerExpansion(path) => { + self.layer_metadata_mut(&path).expanded ^= true; + responses.push_back(DocumentStructureChanged.into()); + responses.push_back(LayerChanged(path).into()) + } + ToggleLayerVisibility(path) => { + responses.push_back(DocumentOperation::ToggleLayerVisibility { path }.into()); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + TransformLayers(message) => self + .transform_layer_handler + .process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses), + Undo => { + responses.push_back(SelectMessage::Abort.into()); + responses.push_back(DocumentHistoryBackward.into()); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + responses.push_back(RenderDocument.into()); + responses.push_back(FolderChanged(vec![]).into()); + } + UngroupLayers(folder_path) => { + // Select all the children of the folder + let to_select = self.graphene_document.folder_children_paths(&folder_path); + let message_buffer = [ + // Copy them + DocumentMessage::SetSelectedLayers(to_select).into(), + PortfolioMessage::Copy(Clipboard::System).into(), + // Paste them into the folder above + PortfolioMessage::PasteIntoFolder { + clipboard: Clipboard::System, + path: folder_path[..folder_path.len() - 1].to_vec(), + insert_index: -1, + } + .into(), + // Delete parent folder + DocumentMessage::DeleteLayer(folder_path).into(), + ]; + + // Push these messages in reverse due to push_front + for message in message_buffer.into_iter().rev() { + responses.push_front(message); + } + } + UngroupSelectedLayers => { + responses.push_back(DocumentMessage::StartTransaction.into()); + let folder_paths = self.graphene_document.sorted_folders_by_depth(self.selected_layers()); + for folder_path in folder_paths { + responses.push_back(DocumentMessage::UngroupLayers(folder_path.to_vec()).into()); + } + responses.push_back(DocumentMessage::CommitTransaction.into()); + } + UpdateLayerMetadata { layer_path: path, layer_metadata } => { + self.layer_metadata.insert(path, layer_metadata); + } ZoomCanvasToFitAll => { if let Some(bounds) = self.document_bounds() { responses.push_back( diff --git a/editor/src/document/movement_handler.rs b/editor/src/document/movement_handler.rs index f5c9e6ac0..9c597909e 100644 --- a/editor/src/document/movement_handler.rs +++ b/editor/src/document/movement_handler.rs @@ -14,38 +14,39 @@ use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +#[remain::sorted] #[impl_message(Message, DocumentMessage, Movement)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum MovementMessage { + DecreaseCanvasZoom { + center_on_mouse: bool, + }, + FitViewportToBounds { + bounds: [DVec2; 2], + padding_scale_factor: Option, + prevent_zoom_past_100: bool, + }, + IncreaseCanvasZoom { + center_on_mouse: bool, + }, MouseMove { snap_angle: Key, wait_for_snap_angle_release: bool, snap_zoom: Key, zoom_from_viewport: Option, }, + RotateCanvasBegin, + SetCanvasRotation(f64), + SetCanvasZoom(f64), + TransformCanvasEnd, + TranslateCanvas(DVec2), TranslateCanvasBegin, + TranslateCanvasByViewportFraction(DVec2), WheelCanvasTranslate { use_y_as_x: bool, }, - RotateCanvasBegin, - ZoomCanvasBegin, - TransformCanvasEnd, - SetCanvasRotation(f64), - SetCanvasZoom(f64), - IncreaseCanvasZoom { - center_on_mouse: bool, - }, - DecreaseCanvasZoom { - center_on_mouse: bool, - }, WheelCanvasZoom, - FitViewportToBounds { - bounds: [DVec2; 2], - padding_scale_factor: Option, - prevent_zoom_past_100: bool, - }, - TranslateCanvas(DVec2), - TranslateCanvasByViewportFraction(DVec2), + ZoomCanvasBegin, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -148,32 +149,54 @@ impl MovementMessageHandler { } impl MessageHandler for MovementMessageHandler { + #[remain::check] fn process_action(&mut self, message: MovementMessage, data: (&Document, &InputPreprocessor), responses: &mut VecDeque) { let (document, ipp) = data; use MovementMessage::*; + #[remain::sorted] match message { - TranslateCanvasBegin => { - self.panning = true; - self.mouse_position = ipp.mouse.position; + DecreaseCanvasZoom { center_on_mouse } => { + let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom); + if center_on_mouse { + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); + } + responses.push_back(SetCanvasZoom(new_scale).into()); } - RotateCanvasBegin => { - self.tilting = true; - self.mouse_position = ipp.mouse.position; - } - ZoomCanvasBegin => { - self.zooming = true; - self.mouse_position = ipp.mouse.position; - } - TransformCanvasEnd => { - self.tilt = self.snapped_angle(); - self.zoom = self.snapped_scale(); + FitViewportToBounds { + bounds: [bounds_corner_a, bounds_corner_b], + padding_scale_factor, + prevent_zoom_past_100, + } => { + let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a); + let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b); + let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO); + let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size()); + + let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5); + let size = (pos2 - pos1) / (v2 - v1); + let size = 1. / size; + let new_scale = size.min_element(); + + self.pan += center; + self.zoom *= new_scale; + + self.zoom /= padding_scale_factor.unwrap_or(1.) as f64; + + if self.zoom > 1. && prevent_zoom_past_100 { + self.zoom = 1. + } + + responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.snap_tilt = false; - self.snap_tilt_released = false; - self.snap_zoom = false; - self.panning = false; - self.tilting = false; - self.zooming = false; + responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); + self.create_document_transform(&ipp.viewport_bounds, responses); + } + IncreaseCanvasZoom { center_on_mouse } => { + let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom); + if center_on_mouse { + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); + } + responses.push_back(SetCanvasZoom(new_scale).into()); } MouseMove { snap_angle, @@ -233,6 +256,16 @@ impl MessageHandler for Moveme } self.mouse_position = ipp.mouse.position; } + RotateCanvasBegin => { + self.tilting = true; + self.mouse_position = ipp.mouse.position; + } + SetCanvasRotation(new_radians) => { + self.tilt = new_radians; + self.create_document_transform(&ipp.viewport_bounds, responses); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into()); + } SetCanvasZoom(new) => { self.zoom = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into()); @@ -240,19 +273,41 @@ impl MessageHandler for Moveme responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } - IncreaseCanvasZoom { center_on_mouse } => { - let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom); - if center_on_mouse { - responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); - } - responses.push_back(SetCanvasZoom(new_scale).into()); + TransformCanvasEnd => { + self.tilt = self.snapped_angle(); + self.zoom = self.snapped_scale(); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + self.snap_tilt = false; + self.snap_tilt_released = false; + self.snap_zoom = false; + self.panning = false; + self.tilting = false; + self.zooming = false; } - DecreaseCanvasZoom { center_on_mouse } => { - let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom); - if center_on_mouse { - responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); - } - responses.push_back(SetCanvasZoom(new_scale).into()); + TranslateCanvas(delta) => { + let transformed_delta = document.root.transform.inverse().transform_vector2(delta); + + self.pan += transformed_delta; + responses.push_back(ToolMessage::DocumentIsDirty.into()); + self.create_document_transform(&ipp.viewport_bounds, responses); + } + TranslateCanvasBegin => { + self.panning = true; + self.mouse_position = ipp.mouse.position; + } + TranslateCanvasByViewportFraction(delta) => { + let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size()); + + self.pan += transformed_delta; + responses.push_back(ToolMessage::DocumentIsDirty.into()); + self.create_document_transform(&ipp.viewport_bounds, responses); + } + WheelCanvasTranslate { use_y_as_x } => { + let delta = match use_y_as_x { + false => -ipp.mouse.scroll_delta.as_dvec2(), + true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(), + } * VIEWPORT_SCROLL_RATE; + responses.push_back(TranslateCanvas(delta).into()); } WheelCanvasZoom => { let scroll = ipp.mouse.scroll_delta.scroll_delta(); @@ -264,61 +319,9 @@ impl MessageHandler for Moveme responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position)); responses.push_back(SetCanvasZoom(self.zoom * zoom_factor).into()); } - WheelCanvasTranslate { use_y_as_x } => { - let delta = match use_y_as_x { - false => -ipp.mouse.scroll_delta.as_dvec2(), - true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(), - } * VIEWPORT_SCROLL_RATE; - responses.push_back(TranslateCanvas(delta).into()); - } - SetCanvasRotation(new_radians) => { - self.tilt = new_radians; - self.create_document_transform(&ipp.viewport_bounds, responses); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into()); - } - FitViewportToBounds { - bounds: [bounds_corner_a, bounds_corner_b], - padding_scale_factor, - prevent_zoom_past_100, - } => { - let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a); - let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b); - let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO); - let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size()); - - let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5); - let size = (pos2 - pos1) / (v2 - v1); - let size = 1. / size; - let new_scale = size.min_element(); - - self.pan += center; - self.zoom *= new_scale; - - self.zoom /= padding_scale_factor.unwrap_or(1.) as f64; - - if self.zoom > 1. && prevent_zoom_past_100 { - self.zoom = 1. - } - - responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); - } - TranslateCanvas(delta) => { - let transformed_delta = document.root.transform.inverse().transform_vector2(delta); - - self.pan += transformed_delta; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); - } - TranslateCanvasByViewportFraction(delta) => { - let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size()); - - self.pan += transformed_delta; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + ZoomCanvasBegin => { + self.zooming = true; + self.mouse_position = ipp.mouse.position; } } } diff --git a/editor/src/document/overlay_message_handler.rs b/editor/src/document/overlay_message_handler.rs index b42064e25..25176c201 100644 --- a/editor/src/document/overlay_message_handler.rs +++ b/editor/src/document/overlay_message_handler.rs @@ -9,11 +9,12 @@ use graphene::document::Document as GrapheneDocument; use graphene::layers::style::ViewMode; use serde::{Deserialize, Serialize}; +#[remain::sorted] #[impl_message(Message, DocumentMessage, Overlay)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum OverlayMessage { - DispatchOperation(Box), ClearAllOverlays, + DispatchOperation(Box), } impl From for OverlayMessage { @@ -28,15 +29,17 @@ pub struct OverlayMessageHandler { } impl MessageHandler for OverlayMessageHandler { + #[remain::check] fn process_action(&mut self, message: OverlayMessage, _data: (&mut LayerMetadata, &Document, &InputPreprocessor), responses: &mut VecDeque) { // let (layer_metadata, document, ipp) = data; use OverlayMessage::*; + #[remain::sorted] match message { + ClearAllOverlays => todo!(), DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(&operation) { Ok(_) => (), Err(e) => log::error!("OverlayError: {:?}", e), }, - ClearAllOverlays => todo!(), } // Render overlays diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index be734dce5..b6eba1e62 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -21,40 +21,41 @@ pub enum Clipboard { const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8; +#[remain::sorted] #[impl_message(Message, Portfolio)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum PortfolioMessage { + AutoSaveActiveDocument, + AutoSaveDocument(u64), + CloseActiveDocumentWithConfirmation, + CloseAllDocuments, + CloseAllDocumentsWithConfirmation, + CloseDocument(u64), + CloseDocumentWithConfirmation(u64), Copy(Clipboard), Cut(Clipboard), - PasteIntoFolder { - clipboard: Clipboard, - path: Vec, - insert_index: isize, - }, - Paste(Clipboard), - SelectDocument(u64), - CloseDocument(u64), #[child] Document(DocumentMessage), - CloseActiveDocumentWithConfirmation, - CloseDocumentWithConfirmation(u64), - CloseAllDocumentsWithConfirmation, - CloseAllDocuments, - RequestAboutGraphiteDialog, NewDocument, + NextDocument, OpenDocument, + OpenDocumentFile(String, String), OpenDocumentFileWithId { document: String, document_name: String, document_id: u64, document_is_saved: bool, }, - OpenDocumentFile(String, String), - UpdateOpenDocumentsList, - AutoSaveDocument(u64), - AutoSaveActiveDocument, - NextDocument, + Paste(Clipboard), + PasteIntoFolder { + clipboard: Clipboard, + path: Vec, + insert_index: isize, + }, PrevDocument, + RequestAboutGraphiteDialog, + SelectDocument(u64), + UpdateOpenDocumentsList, } #[derive(Debug, Clone)] @@ -176,44 +177,31 @@ impl Default for PortfolioMessageHandler { } impl MessageHandler for PortfolioMessageHandler { + #[remain::check] fn process_action(&mut self, message: PortfolioMessage, ipp: &InputPreprocessor, responses: &mut VecDeque) { use DocumentMessage::*; use PortfolioMessage::*; + #[remain::sorted] match message { - RequestAboutGraphiteDialog => { - responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into()); - } - Document(message) => self.active_document_mut().process_action(message, ipp, responses), - SelectDocument(id) => { - let active_document = self.active_document(); - if !active_document.is_saved() { - responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()); - } - self.active_document_id = id; - responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: id }.into()); - responses.push_back(RenderDocument.into()); - responses.push_back(DocumentMessage::DocumentStructureChanged.into()); - for layer in self.active_document().layer_metadata.keys() { - responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into()); - } - responses.push_back(ToolMessage::DocumentIsDirty.into()); + AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()), + AutoSaveDocument(id) => { + let document = self.documents.get(&id).unwrap(); + responses.push_back( + FrontendMessage::TriggerIndexedDbWriteDocument { + document: document.serialize_document(), + details: FrontendDocumentDetails { + is_saved: document.is_saved(), + id, + name: document.name.clone(), + }, + version: GRAPHITE_DOCUMENT_VERSION.to_string(), + } + .into(), + ) } CloseActiveDocumentWithConfirmation => { responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation(self.active_document_id).into()); } - CloseDocumentWithConfirmation(id) => { - let target_document = self.documents.get(&id).unwrap(); - if target_document.is_saved() { - responses.push_back(PortfolioMessage::CloseDocument(id).into()); - } else { - responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id: id }.into()); - // Select the document being closed - responses.push_back(PortfolioMessage::SelectDocument(id).into()); - } - } - CloseAllDocumentsWithConfirmation => { - responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into()); - } CloseAllDocuments => { // Empty the list of internal document data self.documents.clear(); @@ -222,6 +210,9 @@ impl MessageHandler for PortfolioMessageHa // Create a new blank document responses.push_back(NewDocument.into()); } + CloseAllDocumentsWithConfirmation => { + responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into()); + } CloseDocument(id) => { let document_index = self.document_index(id); self.documents.remove(&id); @@ -267,6 +258,37 @@ impl MessageHandler for PortfolioMessageHa responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into()); } } + CloseDocumentWithConfirmation(id) => { + let target_document = self.documents.get(&id).unwrap(); + if target_document.is_saved() { + responses.push_back(PortfolioMessage::CloseDocument(id).into()); + } else { + responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id: id }.into()); + // Select the document being closed + responses.push_back(PortfolioMessage::SelectDocument(id).into()); + } + } + Copy(clipboard) => { + // We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self` + let active_document = self.documents.get(&self.active_document_id).unwrap(); + + let copy_buffer = &mut self.copy_buffer; + copy_buffer[clipboard as usize].clear(); + + for layer_path in active_document.selected_layers_without_children() { + match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) { + (Ok(layer), layer_metadata) => { + copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_metadata }); + } + (Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e), + } + } + } + Cut(clipboard) => { + responses.push_back(Copy(clipboard).into()); + responses.push_back(DeleteSelectedLayers.into()); + } + Document(message) => self.active_document_mut().process_action(message, ipp, responses), NewDocument => { let name = self.generate_new_document_name(); let new_document = DocumentMessageHandler::with_name(name, ipp); @@ -274,6 +296,12 @@ impl MessageHandler for PortfolioMessageHa self.active_document_id = document_id; self.load_document(new_document, document_id, false, responses); } + NextDocument => { + let current_index = self.document_index(self.active_document_id); + let next_index = (current_index + 1) % self.document_ids.len(); + let next_id = self.document_ids[next_index]; + responses.push_back(PortfolioMessage::SelectDocument(next_id).into()); + } OpenDocument => { responses.push_back(FrontendMessage::TriggerFileUpload.into()); } @@ -309,70 +337,6 @@ impl MessageHandler for PortfolioMessageHa ), } } - UpdateOpenDocumentsList => { - // Send the list of document tab names - let open_documents = self - .document_ids - .iter() - .filter_map(|id| { - self.documents.get(id).map(|doc| FrontendDocumentDetails { - is_saved: doc.is_saved(), - id: *id, - name: doc.name.clone(), - }) - }) - .collect::>(); - responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); - } - AutoSaveDocument(id) => { - let document = self.documents.get(&id).unwrap(); - responses.push_back( - FrontendMessage::TriggerIndexedDbWriteDocument { - document: document.serialize_document(), - details: FrontendDocumentDetails { - is_saved: document.is_saved(), - id, - name: document.name.clone(), - }, - version: GRAPHITE_DOCUMENT_VERSION.to_string(), - } - .into(), - ) - } - AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()), - NextDocument => { - let current_index = self.document_index(self.active_document_id); - let next_index = (current_index + 1) % self.document_ids.len(); - let next_id = self.document_ids[next_index]; - responses.push_back(PortfolioMessage::SelectDocument(next_id).into()); - } - PrevDocument => { - let len = self.document_ids.len(); - let current_index = self.document_index(self.active_document_id); - let prev_index = (current_index + len - 1) % len; - let prev_id = self.document_ids[prev_index]; - responses.push_back(PortfolioMessage::SelectDocument(prev_id).into()); - } - Copy(clipboard) => { - // We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self` - let active_document = self.documents.get(&self.active_document_id).unwrap(); - - let copy_buffer = &mut self.copy_buffer; - copy_buffer[clipboard as usize].clear(); - - for layer_path in active_document.selected_layers_without_children() { - match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) { - (Ok(layer), layer_metadata) => { - copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_metadata }); - } - (Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e), - } - } - } - Cut(clipboard) => { - responses.push_back(Copy(clipboard).into()); - responses.push_back(DeleteSelectedLayers.into()); - } Paste(clipboard) => { let document = self.active_document(); let shallowest_common_folder = document @@ -424,6 +388,45 @@ impl MessageHandler for PortfolioMessageHa } } } + PrevDocument => { + let len = self.document_ids.len(); + let current_index = self.document_index(self.active_document_id); + let prev_index = (current_index + len - 1) % len; + let prev_id = self.document_ids[prev_index]; + responses.push_back(PortfolioMessage::SelectDocument(prev_id).into()); + } + RequestAboutGraphiteDialog => { + responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into()); + } + SelectDocument(id) => { + let active_document = self.active_document(); + if !active_document.is_saved() { + responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()); + } + self.active_document_id = id; + responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: id }.into()); + responses.push_back(RenderDocument.into()); + responses.push_back(DocumentMessage::DocumentStructureChanged.into()); + for layer in self.active_document().layer_metadata.keys() { + responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into()); + } + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } + UpdateOpenDocumentsList => { + // Send the list of document tab names + let open_documents = self + .document_ids + .iter() + .filter_map(|id| { + self.documents.get(id).map(|doc| FrontendDocumentDetails { + is_saved: doc.is_saved(), + id: *id, + name: doc.name.clone(), + }) + }) + .collect::>(); + responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); + } } } fn actions(&self) -> ActionList { diff --git a/editor/src/document/transform_layer_handler.rs b/editor/src/document/transform_layer_handler.rs index f2b38902a..13815f68e 100644 --- a/editor/src/document/transform_layer_handler.rs +++ b/editor/src/document/transform_layer_handler.rs @@ -358,25 +358,22 @@ impl Typing { } } +#[remain::sorted] #[impl_message(Message, DocumentMessage, TransformLayers)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum TransformLayerMessage { - BeginGrab, - BeginScale, - BeginRotate, - - CancelOperation, ApplyOperation, - - TypeNumber(u8), + BeginGrab, + BeginRotate, + BeginScale, + CancelOperation, + ConstrainX, + ConstrainY, + MouseMove { slow_key: Key, snap_key: Key }, TypeBackspace, TypeDecimalPoint, TypeNegate, - - ConstrainX, - ConstrainY, - - MouseMove { slow_key: Key, snap_key: Key }, + TypeNumber(u8), } #[derive(Debug, Clone, Default, PartialEq)] @@ -395,6 +392,7 @@ pub struct TransformLayerMessageHandler { } impl MessageHandler, LayerMetadata>, &mut Document, &InputPreprocessor)> for TransformLayerMessageHandler { + #[remain::check] fn process_action(&mut self, message: TransformLayerMessage, data: (&mut HashMap, LayerMetadata>, &mut Document, &InputPreprocessor), responses: &mut VecDeque) { use TransformLayerMessage::*; @@ -413,7 +411,16 @@ impl MessageHandler, LayerMeta *start_mouse = ipp.mouse.position; }; + #[remain::sorted] match message { + ApplyOperation => { + self.original_transforms.clear(); + self.typing.clear(); + + self.operation = Operation::None; + + responses.push_back(ToolMessage::DocumentIsDirty.into()); + } BeginGrab => { if let Operation::Grabbing(_) = self.operation { return; @@ -458,14 +465,8 @@ impl MessageHandler, LayerMeta responses.push_back(ToolMessage::DocumentIsDirty.into()); } - ApplyOperation => { - self.original_transforms.clear(); - self.typing.clear(); - - self.operation = Operation::None; - - responses.push_back(ToolMessage::DocumentIsDirty.into()); - } + ConstrainX => self.operation.constrain_axis(Axis::X, &mut selected, self.snap), + ConstrainY => self.operation.constrain_axis(Axis::Y, &mut selected, self.snap), MouseMove { slow_key, snap_key } => { self.slow = ipp.keyboard.get(slow_key as usize); @@ -515,12 +516,10 @@ impl MessageHandler, LayerMeta } self.mouse_position = ipp.mouse.position; } - TypeNumber(number) => self.operation.handle_typed(self.typing.type_number(number), &mut selected, self.snap), TypeBackspace => self.operation.handle_typed(self.typing.type_backspace(), &mut selected, self.snap), TypeDecimalPoint => self.operation.handle_typed(self.typing.type_decimal_point(), &mut selected, self.snap), TypeNegate => self.operation.handle_typed(self.typing.type_negate(), &mut selected, self.snap), - ConstrainX => self.operation.constrain_axis(Axis::X, &mut selected, self.snap), - ConstrainY => self.operation.constrain_axis(Axis::Y, &mut selected, self.snap), + TypeNumber(number) => self.operation.handle_typed(self.typing.type_number(number), &mut selected, self.snap), } } diff --git a/editor/src/frontend/frontend_message_handler.rs b/editor/src/frontend/frontend_message_handler.rs index e1b17e25b..110c0af9d 100644 --- a/editor/src/frontend/frontend_message_handler.rs +++ b/editor/src/frontend/frontend_message_handler.rs @@ -12,33 +12,36 @@ pub struct FrontendDocumentDetails { pub id: u64, } +#[remain::sorted] #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog - // Update prefix: give the frontend a new value or state for it to use - // Trigger prefix: cause a browser API to do something - DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer }, - UpdateActiveTool { tool_name: String, tool_options: Option }, - UpdateActiveDocument { document_id: u64 }, - UpdateOpenDocumentsList { open_documents: Vec }, - UpdateInputHints { hint_data: HintData }, + DisplayConfirmationToCloseAllDocuments, + DisplayConfirmationToCloseDocument { document_id: u64 }, + DisplayDialogAboutGraphite, DisplayDialogError { title: String, description: String }, DisplayDialogPanic { panic_info: String, title: String, description: String }, - DisplayConfirmationToCloseDocument { document_id: u64 }, - DisplayConfirmationToCloseAllDocuments, - DisplayDialogAboutGraphite, - UpdateDocumentLayer { data: LayerPanelEntry }, - UpdateDocumentArtwork { svg: String }, - UpdateDocumentOverlays { svg: String }, - UpdateDocumentArtboards { svg: String }, - UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) }, - UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 }, - TriggerFileUpload, + DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer }, + + // Trigger prefix: cause a browser API to do something TriggerFileDownload { document: String, name: String }, - TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, + TriggerFileUpload, TriggerIndexedDbRemoveDocument { document_id: u64 }, - UpdateWorkingColors { primary: Color, secondary: Color }, - UpdateCanvasZoom { factor: f64 }, + TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, + + // Update prefix: give the frontend a new value or state for it to use + UpdateActiveDocument { document_id: u64 }, + UpdateActiveTool { tool_name: String, tool_options: Option }, UpdateCanvasRotation { angle_radians: f64 }, + UpdateCanvasZoom { factor: f64 }, + UpdateDocumentArtboards { svg: String }, + UpdateDocumentArtwork { svg: String }, + UpdateDocumentLayer { data: LayerPanelEntry }, + UpdateDocumentOverlays { svg: String }, + UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 }, + UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) }, + UpdateInputHints { hint_data: HintData }, + UpdateOpenDocumentsList { open_documents: Vec }, + UpdateWorkingColors { primary: Color, secondary: Color }, } diff --git a/editor/src/global/global_message_handler.rs b/editor/src/global/global_message_handler.rs index 6fe1fb1e5..21a941f4f 100644 --- a/editor/src/global/global_message_handler.rs +++ b/editor/src/global/global_message_handler.rs @@ -2,11 +2,12 @@ use crate::message_prelude::*; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +#[remain::sorted] #[impl_message(Message, Global)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum GlobalMessage { - LogInfo, LogDebug, + LogInfo, LogTrace, } @@ -14,17 +15,19 @@ pub enum GlobalMessage { pub struct GlobalMessageHandler {} impl MessageHandler for GlobalMessageHandler { + #[remain::check] fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque) { use GlobalMessage::*; + #[remain::sorted] match message { - LogInfo => { - log::set_max_level(log::LevelFilter::Info); - log::info!("set log verbosity to info"); - } LogDebug => { log::set_max_level(log::LevelFilter::Debug); log::info!("set log verbosity to debug"); } + LogInfo => { + log::set_max_level(log::LevelFilter::Info); + log::info!("set log verbosity to info"); + } LogTrace => { log::set_max_level(log::LevelFilter::Trace); log::info!("set log verbosity to trace"); diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index d12868398..001f8fc68 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -14,15 +14,16 @@ use std::fmt::Write; const NUDGE_AMOUNT: f64 = 1.; const SHIFT_NUDGE_AMOUNT: f64 = 10.; +#[remain::sorted] #[impl_message(Message, InputMapper)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum InputMapperMessage { - PointerMove, - MouseScroll, - #[child] - KeyUp(Key), #[child] KeyDown(Key), + #[child] + KeyUp(Key), + MouseScroll, + PointerMove, } #[derive(PartialEq, Clone, Debug)] @@ -116,8 +117,8 @@ macro_rules! mapping { let arr = match entry.trigger { InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], - InputMapperMessage::PointerMove => &mut pointer_move, InputMapperMessage::MouseScroll => &mut mouse_scroll, + InputMapperMessage::PointerMove => &mut pointer_move, }; arr.push(entry.clone()); } @@ -341,8 +342,8 @@ impl Mapping { let list = match message { KeyDown(key) => &self.key_down[key as usize], KeyUp(key) => &self.key_up[key as usize], - PointerMove => &self.pointer_move, MouseScroll => &self.mouse_scroll, + PointerMove => &self.pointer_move, }; list.match_mapping(keys, actions) } diff --git a/editor/src/input/input_preprocessor.rs b/editor/src/input/input_preprocessor.rs index 0fb4140f7..7ad5a4fa2 100644 --- a/editor/src/input/input_preprocessor.rs +++ b/editor/src/input/input_preprocessor.rs @@ -9,16 +9,17 @@ use bitflags::bitflags; pub use graphene::DocumentResponse; use serde::{Deserialize, Serialize}; +#[remain::sorted] #[impl_message(Message, InputPreprocessor)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum InputPreprocessorMessage { + BoundsOfViewports(Vec), + KeyDown(Key, ModifierKeys), + KeyUp(Key, ModifierKeys), MouseDown(EditorMouseState, ModifierKeys), - MouseUp(EditorMouseState, ModifierKeys), MouseMove(EditorMouseState, ModifierKeys), MouseScroll(EditorMouseState, ModifierKeys), - KeyUp(Key, ModifierKeys), - KeyDown(Key, ModifierKeys), - BoundsOfViewports(Vec), + MouseUp(EditorMouseState, ModifierKeys), } bitflags! { @@ -44,55 +45,10 @@ enum KeyPosition { } impl MessageHandler for InputPreprocessor { + #[remain::check] fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque) { + #[remain::sorted] match message { - InputPreprocessorMessage::MouseMove(editor_mouse_state, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - - let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); - self.mouse.position = mouse_state.position; - - responses.push_back(InputMapperMessage::PointerMove.into()); - } - InputPreprocessorMessage::MouseDown(editor_mouse_state, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - - let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); - self.mouse.position = mouse_state.position; - - if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) { - responses.push_back(message); - } - } - InputPreprocessorMessage::MouseUp(editor_mouse_state, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - - let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); - self.mouse.position = mouse_state.position; - - if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Released) { - responses.push_back(message); - } - } - InputPreprocessorMessage::MouseScroll(editor_mouse_state, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - - let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); - self.mouse.position = mouse_state.position; - self.mouse.scroll_delta = mouse_state.scroll_delta; - - responses.push_back(InputMapperMessage::MouseScroll.into()); - } - InputPreprocessorMessage::KeyDown(key, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - self.keyboard.set(key as usize); - responses.push_back(InputMapperMessage::KeyDown(key).into()); - } - InputPreprocessorMessage::KeyUp(key, modifier_keys) => { - self.handle_modifier_keys(modifier_keys, responses); - self.keyboard.unset(key as usize); - responses.push_back(InputMapperMessage::KeyUp(key).into()); - } InputPreprocessorMessage::BoundsOfViewports(bounds_of_viewports) => { assert_eq!(bounds_of_viewports.len(), 1, "Only one viewport is currently supported"); @@ -134,6 +90,53 @@ impl MessageHandler for InputPreprocessor { ); } } + InputPreprocessorMessage::KeyDown(key, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + self.keyboard.set(key as usize); + responses.push_back(InputMapperMessage::KeyDown(key).into()); + } + InputPreprocessorMessage::KeyUp(key, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + self.keyboard.unset(key as usize); + responses.push_back(InputMapperMessage::KeyUp(key).into()); + } + InputPreprocessorMessage::MouseDown(editor_mouse_state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + + if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) { + responses.push_back(message); + } + } + InputPreprocessorMessage::MouseMove(editor_mouse_state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + + responses.push_back(InputMapperMessage::PointerMove.into()); + } + InputPreprocessorMessage::MouseScroll(editor_mouse_state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + self.mouse.scroll_delta = mouse_state.scroll_delta; + + responses.push_back(InputMapperMessage::MouseScroll.into()); + } + InputPreprocessorMessage::MouseUp(editor_mouse_state, modifier_keys) => { + self.handle_modifier_keys(modifier_keys, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + + if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Released) { + responses.push_back(message); + } + } }; } // clean user input and if possible reconstruct it diff --git a/editor/src/tool/tool_message_handler.rs b/editor/src/tool/tool_message_handler.rs index 563a95a07..9879e7902 100644 --- a/editor/src/tool/tool_message_handler.rs +++ b/editor/src/tool/tool_message_handler.rs @@ -9,63 +9,55 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +#[remain::sorted] #[impl_message(Message, Tool)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ToolMessage { - SelectPrimaryColor(Color), - SelectSecondaryColor(Color), - SwapColors, - ResetColors, - NoOp, ActivateTool(ToolType), + #[child] + Crop(CropMessage), DocumentIsDirty, - UpdateHints, - SetToolOptions(ToolType, ToolOptions), - #[child] - Fill(FillMessage), - #[child] - Rectangle(RectangleMessage), #[child] Ellipse(EllipseMessage), #[child] - Select(SelectMessage), + Eyedropper(EyedropperMessage), + #[child] + Fill(FillMessage), #[child] Line(LineMessage), #[child] - Crop(CropMessage), - #[child] - Eyedropper(EyedropperMessage), - #[child] Navigate(NavigateMessage), + NoOp, #[child] Path(PathMessage), #[child] Pen(PenMessage), #[child] + Rectangle(RectangleMessage), + ResetColors, + #[child] + Select(SelectMessage), + SelectPrimaryColor(Color), + SelectSecondaryColor(Color), + SetToolOptions(ToolType, ToolOptions), + #[child] Shape(ShapeMessage), + SwapColors, + UpdateHints, } #[derive(Debug, Default)] pub struct ToolMessageHandler { tool_state: ToolFsmState, } + impl MessageHandler for ToolMessageHandler { + #[remain::check] fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque) { let (document, input) = data; use ToolMessage::*; + #[remain::sorted] match message { - SelectPrimaryColor(color) => { - let document_data = &mut self.tool_state.document_tool_data; - document_data.primary_color = color; - - update_working_colors(&self.tool_state.document_tool_data, responses); - } - SelectSecondaryColor(color) => { - let document_data = &mut self.tool_state.document_tool_data; - document_data.secondary_color = color; - - update_working_colors(document_data, responses); - } ActivateTool(new_tool) => { let tool_data = &mut self.tool_state.tool_data; let document_data = &self.tool_state.document_tool_data; @@ -114,13 +106,6 @@ impl MessageHandler responses.push_back(message.into()); } } - SwapColors => { - let document_data = &mut self.tool_state.document_tool_data; - - std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color); - - update_working_colors(document_data, responses); - } ResetColors => { let document_data = &mut self.tool_state.document_tool_data; @@ -129,24 +114,44 @@ impl MessageHandler update_working_colors(document_data, responses); } + SelectPrimaryColor(color) => { + let document_data = &mut self.tool_state.document_tool_data; + document_data.primary_color = color; + + update_working_colors(&self.tool_state.document_tool_data, responses); + } + SelectSecondaryColor(color) => { + let document_data = &mut self.tool_state.document_tool_data; + document_data.secondary_color = color; + + update_working_colors(document_data, responses); + } SetToolOptions(tool_type, tool_options) => { let document_data = &mut self.tool_state.document_tool_data; document_data.tool_options.insert(tool_type, tool_options); } - message => { - let tool_type = message_to_tool_type(&message); + SwapColors => { + let document_data = &mut self.tool_state.document_tool_data; + + std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color); + + update_working_colors(document_data, responses); + } + tool_message => { + let tool_type = message_to_tool_type(&tool_message); let document_data = &self.tool_state.document_tool_data; let tool_data = &mut self.tool_state.tool_data; if let Some(tool) = tool_data.tools.get_mut(&tool_type) { if tool_type == tool_data.active_tool_type { - tool.process_action(message, (document, document_data, input), responses); + tool.process_action(tool_message, (document, document_data, input), responses); } } } } } + fn actions(&self) -> ActionList { let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions); list.extend(self.tool_state.tool_data.active_tool().actions()); diff --git a/editor/src/tool/tools/crop.rs b/editor/src/tool/tools/crop.rs index e3573bae9..b92136b3b 100644 --- a/editor/src/tool/tools/crop.rs +++ b/editor/src/tool/tools/crop.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Crop; +#[remain::sorted] #[impl_message(Message, ToolMessage, Crop)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum CropMessage { diff --git a/editor/src/tool/tools/ellipse.rs b/editor/src/tool/tools/ellipse.rs index 1a9029801..a102c7151 100644 --- a/editor/src/tool/tools/ellipse.rs +++ b/editor/src/tool/tools/ellipse.rs @@ -13,13 +13,14 @@ pub struct Ellipse { data: EllipseToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Ellipse)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum EllipseMessage { + Abort, DragStart, DragStop, Resize { center: Key, lock_ratio: Key }, - Abort, } impl<'a> MessageHandler> for Ellipse { diff --git a/editor/src/tool/tools/eyedropper.rs b/editor/src/tool/tools/eyedropper.rs index e7b8a69e8..0dba1d57a 100644 --- a/editor/src/tool/tools/eyedropper.rs +++ b/editor/src/tool/tools/eyedropper.rs @@ -15,12 +15,13 @@ pub struct Eyedropper { data: EyedropperToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Eyedropper)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum EyedropperMessage { + Abort, LeftMouseDown, RightMouseDown, - Abort, } impl<'a> MessageHandler> for Eyedropper { diff --git a/editor/src/tool/tools/fill.rs b/editor/src/tool/tools/fill.rs index b5c734f2a..0185ebfe5 100644 --- a/editor/src/tool/tools/fill.rs +++ b/editor/src/tool/tools/fill.rs @@ -15,12 +15,13 @@ pub struct Fill { data: FillToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Fill)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum FillMessage { + Abort, LeftMouseDown, RightMouseDown, - Abort, } impl<'a> MessageHandler> for Fill { diff --git a/editor/src/tool/tools/line.rs b/editor/src/tool/tools/line.rs index c348a8d6b..215cfc789 100644 --- a/editor/src/tool/tools/line.rs +++ b/editor/src/tool/tools/line.rs @@ -15,13 +15,14 @@ pub struct Line { data: LineToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Line)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum LineMessage { + Abort, DragStart, DragStop, Redraw { center: Key, lock_angle: Key, snap_angle: Key }, - Abort, } impl<'a> MessageHandler> for Line { diff --git a/editor/src/tool/tools/navigate.rs b/editor/src/tool/tools/navigate.rs index 85a650971..6f1ac5d04 100644 --- a/editor/src/tool/tools/navigate.rs +++ b/editor/src/tool/tools/navigate.rs @@ -11,16 +11,17 @@ pub struct Navigate { data: NavigateToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Navigate)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum NavigateMessage { + Abort, ClickZoom { zoom_in: bool }, MouseMove { snap_angle: Key, snap_zoom: Key }, - TranslateCanvasBegin, RotateCanvasBegin, - ZoomCanvasBegin, TransformCanvasEnd, - Abort, + TranslateCanvasBegin, + ZoomCanvasBegin, } impl<'a> MessageHandler> for Navigate { diff --git a/editor/src/tool/tools/path.rs b/editor/src/tool/tools/path.rs index bb360192d..b6ee0e9e0 100644 --- a/editor/src/tool/tools/path.rs +++ b/editor/src/tool/tools/path.rs @@ -27,16 +27,17 @@ pub struct Path { data: PathToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Path)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum PathMessage { - DragStart, - PointerMove, - DragStop, - // Standard messages Abort, DocumentIsDirty, + + DragStart, + DragStop, + PointerMove, } impl<'a> MessageHandler> for Path { diff --git a/editor/src/tool/tools/pen.rs b/editor/src/tool/tools/pen.rs index 67abe7f02..3944a9fbf 100644 --- a/editor/src/tool/tools/pen.rs +++ b/editor/src/tool/tools/pen.rs @@ -14,15 +14,16 @@ pub struct Pen { data: PenToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Pen)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum PenMessage { - Undo, + Abort, + Confirm, DragStart, DragStop, PointerMove, - Confirm, - Abort, + Undo, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/editor/src/tool/tools/rectangle.rs b/editor/src/tool/tools/rectangle.rs index 600ab27be..349ae6768 100644 --- a/editor/src/tool/tools/rectangle.rs +++ b/editor/src/tool/tools/rectangle.rs @@ -15,13 +15,14 @@ pub struct Rectangle { data: RectangleToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Rectangle)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum RectangleMessage { + Abort, DragStart, DragStop, Resize { center: Key, lock_ratio: Key }, - Abort, } impl<'a> MessageHandler> for Rectangle { diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index be95ef73e..dcaafa746 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -26,6 +26,7 @@ pub struct Select { data: SelectToolData, } +// #[remain::sorted] // https://github.com/dtolnay/remain/issues/16 #[impl_message(Message, ToolMessage, Select)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum SelectMessage { diff --git a/editor/src/tool/tools/shape.rs b/editor/src/tool/tools/shape.rs index dc668d7e3..6de2eb972 100644 --- a/editor/src/tool/tools/shape.rs +++ b/editor/src/tool/tools/shape.rs @@ -15,13 +15,14 @@ pub struct Shape { data: ShapeToolData, } +#[remain::sorted] #[impl_message(Message, ToolMessage, Shape)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum ShapeMessage { + Abort, DragStart, DragStop, Resize { center: Key, lock_ratio: Key }, - Abort, } impl<'a> MessageHandler> for Shape {