use crate::document::PortfolioMessageHandler; use crate::global::GlobalMessageHandler; use crate::input::{InputMapperMessageHandler, InputPreprocessorMessageHandler}; use crate::message_prelude::*; use crate::viewport_tools::tool_message_handler::ToolMessageHandler; use std::collections::VecDeque; #[derive(Debug, Default)] pub struct Dispatcher { message_queue: VecDeque, pub responses: Vec, message_handlers: DispatcherMessageHandlers, } #[remain::sorted] #[derive(Debug, Default)] struct DispatcherMessageHandlers { global_message_handler: GlobalMessageHandler, input_mapper_message_handler: InputMapperMessageHandler, input_preprocessor_message_handler: InputPreprocessorMessageHandler, portfolio_message_handler: PortfolioMessageHandler, tool_message_handler: ToolMessageHandler, } // For optimization, these are messages guaranteed to be redundant when repeated. // The last occurrence of the message in the message queue is sufficient to ensure correct behavior. // In addition, these messages do not change any state in the backend (aside from caches). const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayDocumentLayerTreeStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList), MessageDiscriminant::Tool(ToolMessageDiscriminant::DocumentIsDirty), ]; impl Dispatcher { pub fn new() -> Self { Self::default() } #[remain::check] pub fn handle_message>(&mut self, message: T) { use Message::*; self.message_queue.push_back(message.into()); while let Some(message) = self.message_queue.pop_front() { // Skip processing of this message if it will be processed later if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) && self.message_queue.contains(&message) { continue; } // Print the message at a verbosity level of `log` self.log_message(&message); // Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend #[remain::sorted] match message { Frontend(message) => { // `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed self.responses.push(message); } Global(message) => { self.message_handlers.global_message_handler.process_action(message, (), &mut self.message_queue); } InputMapper(message) => { let actions = self.collect_actions(); self.message_handlers .input_mapper_message_handler .process_action(message, (&self.message_handlers.input_preprocessor_message_handler, actions), &mut self.message_queue); } InputPreprocessor(message) => { self.message_handlers.input_preprocessor_message_handler.process_action(message, (), &mut self.message_queue); } NoOp => {} Portfolio(message) => { self.message_handlers .portfolio_message_handler .process_action(message, &self.message_handlers.input_preprocessor_message_handler, &mut self.message_queue); } Tool(message) => { self.message_handlers.tool_message_handler.process_action( message, ( self.message_handlers.portfolio_message_handler.active_document(), &self.message_handlers.input_preprocessor_message_handler, ), &mut self.message_queue, ); } } } } pub fn collect_actions(&self) -> ActionList { // TODO: Reduce the number of heap allocations let mut list = Vec::new(); list.extend(self.message_handlers.input_preprocessor_message_handler.actions()); list.extend(self.message_handlers.input_mapper_message_handler.actions()); list.extend(self.message_handlers.global_message_handler.actions()); list.extend(self.message_handlers.tool_message_handler.actions()); list.extend(self.message_handlers.portfolio_message_handler.actions()); list } fn log_message(&self, message: &Message) { use Message::*; if log::max_level() == log::LevelFilter::Trace && !(matches!( message, InputPreprocessor(_) | Frontend(FrontendMessage::UpdateCanvasZoom { .. }) | Frontend(FrontendMessage::UpdateCanvasRotation { .. }) ) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove")) { log::trace!("Message: {:?}", message); // log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions())); } } } #[cfg(test)] mod test { use crate::communication::set_uuid_seed; use crate::document::clipboards::Clipboard; use crate::document::DocumentMessageHandler; use crate::message_prelude::*; use crate::misc::test_utils::EditorTestUtils; use crate::Editor; use graphene::color::Color; use graphene::Operation; fn init_logger() { let _ = env_logger::builder().is_test(true).try_init(); } /// Create an editor instance with three layers /// 1. A red rectangle /// 2. A blue shape /// 3. A green ellipse fn create_editor_with_three_layers() -> Editor { set_uuid_seed(0); let mut editor = Editor::new(); editor.select_primary_color(Color::RED); editor.draw_rect(100., 200., 300., 400.); editor.select_primary_color(Color::BLUE); editor.draw_shape(10., 1200., 1300., 400.); editor.select_primary_color(Color::GREEN); editor.draw_ellipse(104., 1200., 1300., 400.); editor } #[test] /// - create rect, shape and ellipse /// - copy /// - paste /// - assert that ellipse was copied fn copy_paste_single_layer() { init_logger(); let mut editor = create_editor_with_three_layers(); let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User }); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 4); // Existing layers are unaffected for i in 0..=2 { assert_eq!(layers_before_copy[i], layers_after_copy[i]); } // The ellipse was copied assert_eq!(layers_before_copy[2], layers_after_copy[3]); } #[test] /// - create rect, shape and ellipse /// - select shape /// - copy /// - paste /// - assert that shape was copied fn copy_paste_single_layer_from_middle() { init_logger(); let mut editor = create_editor_with_three_layers(); let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1]; editor.handle_message(DocumentMessage::SetSelectedLayers { replacement_selected_layers: vec![vec![shape_id]], }); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User }); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 4); // Existing layers are unaffected for i in 0..=2 { assert_eq!(layers_before_copy[i], layers_after_copy[i]); } // The shape was copied assert_eq!(layers_before_copy[1], layers_after_copy[3]); } #[test] fn copy_paste_folder() { init_logger(); let mut editor = create_editor_with_three_layers(); const FOLDER_INDEX: usize = 3; const ELLIPSE_INDEX: usize = 2; const SHAPE_INDEX: usize = 1; const RECT_INDEX: usize = 0; const LINE_INDEX: usize = 0; const PEN_INDEX: usize = 1; editor.handle_message(DocumentMessage::CreateEmptyFolder { container_path: vec![] }); let document_before_added_shapes = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX]; // TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils. // This has not been done yet as the line and pen tool are not yet able to add layers to the currently selected folder editor.handle_message(Operation::AddLine { path: vec![folder_id, LINE_INDEX as u64], insert_index: 0, transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], style: Default::default(), }); editor.handle_message(Operation::AddPen { path: vec![folder_id, PEN_INDEX as u64], insert_index: 0, transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], style: Default::default(), points: vec![(10.0, 20.0), (30.0, 40.0)], }); editor.handle_message(DocumentMessage::SetSelectedLayers { replacement_selected_layers: vec![vec![folder_id]], }); let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User }); editor.handle_message(DocumentMessage::DeleteSelectedLayers); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); assert_eq!(layers_before_copy.len(), 4); assert_eq!(layers_after_copy.len(), 5); let rect_before_copy = &layers_before_copy[RECT_INDEX]; let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX]; let shape_before_copy = &layers_before_copy[SHAPE_INDEX]; let folder_before_copy = &layers_before_copy[FOLDER_INDEX]; let line_before_copy = folder_before_copy.as_folder().unwrap().layers()[LINE_INDEX].clone(); let pen_before_copy = folder_before_copy.as_folder().unwrap().layers()[PEN_INDEX].clone(); assert_eq!(&layers_after_copy[0], rect_before_copy); assert_eq!(&layers_after_copy[1], shape_before_copy); assert_eq!(&layers_after_copy[2], ellipse_before_copy); assert_eq!(&layers_after_copy[3], folder_before_copy); assert_eq!(&layers_after_copy[4], folder_before_copy); // Check the layers inside the two folders let first_folder_layers_after_copy = layers_after_copy[3].as_folder().unwrap().layers(); let second_folder_layers_after_copy = layers_after_copy[4].as_folder().unwrap().layers(); assert_eq!(first_folder_layers_after_copy.len(), 2); assert_eq!(second_folder_layers_after_copy.len(), 2); assert_eq!(first_folder_layers_after_copy[0], line_before_copy); assert_eq!(first_folder_layers_after_copy[1], pen_before_copy); assert_eq!(second_folder_layers_after_copy[0], line_before_copy); assert_eq!(second_folder_layers_after_copy[1], pen_before_copy); } #[test] /// - create rect, shape and ellipse /// - select ellipse and rect /// - copy /// - delete /// - create another rect /// - paste /// - paste fn copy_paste_deleted_layers() { init_logger(); let mut editor = create_editor_with_three_layers(); const ELLIPSE_INDEX: usize = 2; const SHAPE_INDEX: usize = 1; const RECT_INDEX: usize = 0; let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX]; let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX]; editor.handle_message(DocumentMessage::SetSelectedLayers { replacement_selected_layers: vec![vec![rect_id], vec![ellipse_id]], }); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::User }); editor.handle_message(DocumentMessage::DeleteSelectedLayers); editor.draw_rect(0., 800., 12., 200.); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::User, folder_path: vec![], insert_index: -1, }); let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); assert_eq!(layers_before_copy.len(), 3); assert_eq!(layers_after_copy.len(), 6); let rect_before_copy = &layers_before_copy[RECT_INDEX]; let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX]; assert_eq!(layers_after_copy[0], layers_before_copy[SHAPE_INDEX]); assert_eq!(&layers_after_copy[2], rect_before_copy); assert_eq!(&layers_after_copy[3], ellipse_before_copy); assert_eq!(&layers_after_copy[4], rect_before_copy); assert_eq!(&layers_after_copy[5], ellipse_before_copy); } #[test] #[ignore] // TODO: Re-enable test, see issue #444 (https://github.com/GraphiteEditor/Graphite/pull/444) /// - create rect, shape and ellipse /// - select ellipse and rect /// - move them down and back up again fn move_selection() { init_logger(); let mut editor = create_editor_with_three_layers(); fn map_to_vec(paths: Vec<&[LayerId]>) -> Vec> { paths.iter().map(|layer| layer.to_vec()).collect::>() } let sorted_layers = map_to_vec(editor.dispatcher.message_handlers.portfolio_message_handler.active_document().all_layers_sorted()); println!("Sorted layers: {:?}", sorted_layers); let verify_order = |handler: &mut DocumentMessageHandler| { ( map_to_vec(handler.all_layers_sorted()), map_to_vec(handler.non_selected_layers_sorted()), map_to_vec(handler.selected_layers_sorted()), ) }; editor.handle_message(DocumentMessage::SetSelectedLayers { replacement_selected_layers: sorted_layers[..2].to_vec(), }); editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut()); assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::>()); editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); } }