diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index b39d1d52..734742e6 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -19,6 +19,8 @@ use std::collections::VecDeque; use super::movement_handler::{MovementMessage, MovementMessageHandler}; +type DocumentSave = (InternalDocument, HashMap, LayerData>); + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] pub enum FlipAxis { X, @@ -42,7 +44,8 @@ pub enum AlignAggregate { #[derive(Clone, Debug)] pub struct DocumentMessageHandler { pub document: InternalDocument, - pub document_backup: Option, + pub document_history: Vec, + pub document_redo_history: Vec, pub name: String, pub layer_data: HashMap, LayerData>, movement_handler: MovementMessageHandler, @@ -52,7 +55,8 @@ impl Default for DocumentMessageHandler { fn default() -> Self { Self { document: InternalDocument::default(), - document_backup: None, + document_history: Vec::new(), + document_redo_history: Vec::new(), name: String::from("Untitled Document"), layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(), movement_handler: MovementMessageHandler::default(), @@ -90,6 +94,10 @@ pub enum DocumentMessage { SaveDocument, RenderDocument, Undo, + Redo, + DocumentHistoryBackward, + DocumentHistoryForward, + ClearOverlays, NudgeSelectedLayers(f64, f64), AlignSelectedLayers(AlignAxis, AlignAggregate), MoveSelectedLayersTo { @@ -195,7 +203,8 @@ impl DocumentMessageHandler { pub fn with_name(name: String) -> Self { Self { document: InternalDocument::default(), - document_backup: None, + document_history: Vec::new(), + document_redo_history: Vec::new(), name, layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(), movement_handler: MovementMessageHandler::default(), @@ -219,18 +228,42 @@ impl DocumentMessageHandler { } pub fn backup(&mut self) { - self.document_backup = Some(self.document.clone()) + self.document_redo_history.clear(); + let new_layer_data = self + .layer_data + .iter() + .filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value))) + .collect(); + self.document_history.push((self.document.clone_without_overlays(), new_layer_data)) } pub fn rollback(&mut self) -> Result<(), EditorError> { self.backup(); - self.reset() + self.undo() } - pub fn reset(&mut self) -> Result<(), EditorError> { - match self.document_backup.take() { - Some(backup) => { - self.document = backup; + pub fn undo(&mut self) -> Result<(), EditorError> { + match self.document_history.pop() { + Some((document, layer_data)) => { + let document = std::mem::replace(&mut self.document, document); + let layer_data = std::mem::replace(&mut self.layer_data, layer_data); + self.document_redo_history.push((document, layer_data)); + Ok(()) + } + None => Err(EditorError::NoTransactionInProgress), + } + } + + pub fn redo(&mut self) -> Result<(), EditorError> { + match self.document_redo_history.pop() { + Some((document, layer_data)) => { + let document = std::mem::replace(&mut self.document, document); + let layer_data = std::mem::replace(&mut self.layer_data, layer_data); + let new_layer_data = layer_data + .iter() + .filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value))) + .collect(); + self.document_history.push((document.clone_without_overlays(), new_layer_data)); Ok(()) } None => Err(EditorError::NoTransactionInProgress), @@ -284,10 +317,10 @@ impl MessageHandler for DocumentMessageHand responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); } AbortTransaction => { - self.reset().unwrap_or_else(|e| log::warn!("{}", e)); + self.undo().unwrap_or_else(|e| log::warn!("{}", e)); responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); } - CommitTransaction => self.document_backup = None, + CommitTransaction => (), ExportDocument => { let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); let size = bbox[1] - bbox[0]; @@ -325,11 +358,13 @@ impl MessageHandler for DocumentMessageHand ) } SetBlendModeForSelectedLayers(blend_mode) => { + self.backup(); for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) { responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into()); } } SetOpacityForSelectedLayers(opacity) => { + self.backup(); let opacity = opacity.clamp(0., 1.); for path in self.selected_layers().cloned() { @@ -345,12 +380,20 @@ impl MessageHandler for DocumentMessageHand } SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()), DeleteSelectedLayers => { + self.backup(); + responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); for path in self.selected_layers().cloned() { - responses.push_back(DocumentOperation::DeleteLayer { path }.into()) + responses.push_front(DocumentOperation::DeleteLayer { path }.into()) + } + } + ClearOverlays => { + responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); + for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() { + responses.push_front(DocumentOperation::DeleteLayer { path }.into()) } - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); } DuplicateSelectedLayers => { + self.backup(); for path in self.selected_layers_sorted() { responses.push_back(DocumentOperation::DuplicateLayer { path }.into()) } @@ -365,7 +408,7 @@ impl MessageHandler for DocumentMessageHand } // TODO: Correctly update layer panel in clear_selection instead of here responses.extend(self.handle_folder_changed(Vec::new())); - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); } SelectAllLayers => { let all_layer_paths = self @@ -374,16 +417,26 @@ impl MessageHandler for DocumentMessageHand .filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay) .cloned() .collect::>(); - responses.push_back(SetSelectedLayers(all_layer_paths).into()); + responses.push_front(SetSelectedLayers(all_layer_paths).into()); } DeselectAllLayers => { - responses.push_back(SetSelectedLayers(vec![]).into()); + 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 => { - // this is a temporary fix and will be addressed by #123 - if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() { - responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into()) - } + responses.push_back(SelectMessage::Abort.into()); + responses.push_back(DocumentHistoryBackward.into()); + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(RenderDocument.into()); + responses.push_back(FolderChanged(vec![]).into()); + } + Redo => { + responses.push_back(SelectMessage::Abort.into()); + responses.push_back(DocumentHistoryForward.into()); + responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(RenderDocument.into()); + responses.push_back(FolderChanged(vec![]).into()); } FolderChanged(path) => responses.extend(self.handle_folder_changed(path)), DispatchOperation(op) => match self.document.handle_operation(&op) { @@ -446,6 +499,7 @@ impl MessageHandler for DocumentMessageHand } NudgeSelectedLayers(x, y) => { + self.backup(); for path in self.selected_layers().cloned() { let operation = DocumentOperation::TransformLayerInViewport { path, @@ -461,6 +515,7 @@ impl MessageHandler for DocumentMessageHand responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into()); } ReorderSelectedLayers(relative_position) => { + self.backup(); let all_layer_paths = self.all_layers_sorted(); let selected_layers = self.selected_layers_sorted(); if let Some(pivot) = match relative_position.signum() { @@ -491,6 +546,7 @@ impl MessageHandler for DocumentMessageHand } } FlipSelectedLayers(axis) => { + self.backup(); let scale = match axis { FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::Y => DVec2::new(1., -1.), @@ -512,6 +568,7 @@ impl MessageHandler for DocumentMessageHand } } AlignSelectedLayers(axis, aggregate) => { + self.backup(); let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip(); let axis = match axis { @@ -550,6 +607,7 @@ impl MessageHandler for DocumentMessageHand fn actions(&self) -> ActionList { let mut common = actions!(DocumentMessageDiscriminant; Undo, + Redo, SelectAllLayers, DeselectAllLayers, RenderDocument, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index be9cd256..91189af6 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -185,6 +185,7 @@ impl Default for Mapping { // Editor Actions entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]}, // Document Actions + 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]}, diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index f2a34a7a..ccab7a0b 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -89,9 +89,9 @@ impl SelectToolData { } } -fn add_boundnig_box(responses: &mut VecDeque) -> Vec { +fn add_boundnig_box(responses: &mut Vec) -> Vec { let path = vec![generate_uuid()]; - responses.push_back( + responses.push( Operation::AddBoundingBox { path: path.clone(), transform: DAffine2::ZERO.to_cols_array(), @@ -124,22 +124,25 @@ impl Fsm for SelectToolFsmState { if let ToolMessage::Select(event) = event { match (self, event) { (_, UpdateSelectionBoundingBox) => { + let mut buffer = Vec::new(); let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) { (None, Some(path)) => Operation::DeleteLayer { path }.into(), (Some([pos1, pos2]), path) => { - let path = path.unwrap_or_else(|| add_boundnig_box(responses)); + let path = path.unwrap_or_else(|| add_boundnig_box(&mut buffer)); data.bounding_box_id = Some(path.clone()); let transform = transform_from_box(pos1, pos2); Operation::SetLayerTransformInViewport { path, transform }.into() } (_, _) => Message::NoOp, }; - responses.push_back(response); + responses.push_front(response); + buffer.into_iter().rev().for_each(|message| responses.push_front(message)); self } (Ready, DragStart { add_to_selection }) => { 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 quad = data.selection_quad(); let intersection = document.document.intersects_quad_root(quad); @@ -147,25 +150,29 @@ impl Fsm for SelectToolFsmState { if selected.is_empty() { if let Some(layer) = intersection.last() { selected.push(layer.clone()); - responses.push_back(DocumentMessage::SetSelectedLayers(selected.clone()).into()); + buffer.push(DocumentMessage::SetSelectedLayers(selected.clone()).into()); } } // If the user clicks on a layer that is in their current selection, go into the dragging mode. // Otherwise enter the box select mode - if selected.iter().any(|path| intersection.contains(path)) { + let state = if selected.iter().any(|path| intersection.contains(path)) { + buffer.push(DocumentMessage::StartTransaction.into()); data.layers_dragging = selected; Dragging } else { if !input.keyboard.get(add_to_selection as usize) { - responses.push_back(DocumentMessage::DeselectAllLayers.into()); + buffer.push(DocumentMessage::DeselectAllLayers.into()); } - data.drag_box_id = Some(add_boundnig_box(responses)); + data.drag_box_id = Some(add_boundnig_box(&mut buffer)); DrawingBox - } + }; + buffer.into_iter().rev().for_each(|message| responses.push_front(message)); + state } (Dragging, MouseMove) => { + responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); for path in data.layers_dragging.iter() { - responses.push_back( + responses.push_front( Operation::TransformLayerInViewport { path: path.clone(), transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(), @@ -173,7 +180,6 @@ impl Fsm for SelectToolFsmState { .into(), ); } - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); data.drag_current = input.mouse.position; Dragging } @@ -183,7 +189,7 @@ impl Fsm for SelectToolFsmState { let start = data.drag_start + half_pixel_offset; let size = data.drag_current - start + half_pixel_offset; - responses.push_back( + responses.push_front( Operation::SetLayerTransformInViewport { path: data.drag_box_id.clone().unwrap(), transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(), @@ -192,21 +198,27 @@ impl Fsm for SelectToolFsmState { ); DrawingBox } - (Dragging, DragStop) => Ready, + (Dragging, DragStop) => { + let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON { + true => DocumentMessage::Undo, + false => DocumentMessage::CommitTransaction, + }; + responses.push_front(response.into()); + Ready + } (DrawingBox, DragStop) => { let quad = data.selection_quad(); - responses.push_back(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into()); - responses.push_back( + responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into()); + responses.push_front( Operation::DeleteLayer { path: data.drag_box_id.take().unwrap(), } .into(), ); - data.drag_box_id = None; Ready } (_, Abort) => { - let mut delete = |path: &mut Option>| path.take().map(|path| responses.push_back(Operation::DeleteLayer { path }.into())); + let mut delete = |path: &mut Option>| path.take().map(|path| responses.push_front(Operation::DeleteLayer { path }.into())); delete(&mut data.drag_box_id); delete(&mut data.bounding_box_id); Ready diff --git a/frontend/src/components/widgets/inputs/MenuBarInput.vue b/frontend/src/components/widgets/inputs/MenuBarInput.vue index f6a40959..79924fbe 100644 --- a/frontend/src/components/widgets/inputs/MenuBarInput.vue +++ b/frontend/src/components/widgets/inputs/MenuBarInput.vue @@ -108,7 +108,7 @@ const menuEntries: MenuListEntries = [ children: [ [ { label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() }, - { label: "Redo", shortcut: ["Ctrl", "⇧", "Z"] }, + { label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => (await wasm).redo() }, ], [ { label: "Cut", shortcut: ["Ctrl", "X"] }, diff --git a/frontend/wasm/src/document.rs b/frontend/wasm/src/document.rs index 62e98b60..98785e5a 100644 --- a/frontend/wasm/src/document.rs +++ b/frontend/wasm/src/document.rs @@ -212,6 +212,12 @@ pub fn undo() -> Result<(), JsValue> { EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error) } +/// Redo history one step +#[wasm_bindgen] +pub fn redo() -> Result<(), JsValue> { + EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Redo)).map_err(convert_error) +} + /// Select all layers #[wasm_bindgen] pub fn select_all_layers() -> Result<(), JsValue> { diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 582ebad5..8906a467 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -272,6 +272,24 @@ impl Document { self.set_transform_relative_to_scope(layer, None, transform) } + fn remove_overlays(&mut self, path: &mut Vec) { + if self.layer(path).unwrap().overlay { + self.delete(path).unwrap() + } + let ids = self.folder(path).map(|folder| folder.layer_ids.clone()).unwrap_or_default(); + for id in ids { + path.push(id); + self.remove_overlays(path); + path.pop(); + } + } + + pub fn clone_without_overlays(&self) -> Self { + let mut document = self.clone(); + document.remove_overlays(&mut vec![]); + document + } + /// Mutate the document by applying the `operation` to it. If the operation necessitates a /// reaction from the frontend, responses may be returned. pub fn handle_operation(&mut self, operation: &Operation) -> Result>, DocumentError> {