diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 7183dcb0ce..5694e70cb7 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -1,4 +1,5 @@ use super::broadcast_message_handler::BroadcastMessageHandler; +use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE}; use crate::document::PortfolioMessageHandler; use crate::global::GlobalMessageHandler; use crate::input::{InputMapperMessageHandler, InputPreprocessorMessageHandler}; @@ -7,6 +8,8 @@ use crate::message_prelude::*; use crate::viewport_tools::tool_message_handler::ToolMessageHandler; use crate::workspace::WorkspaceMessageHandler; +use graphene::layers::text_layer::Font; + use std::collections::VecDeque; #[derive(Debug, Default)] @@ -42,8 +45,6 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure), - MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateActiveDocument), - MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerSignal(BroadcastSignalDiscriminant::DocumentIsDirty)), ]; @@ -87,6 +88,18 @@ impl Dispatcher { match message { #[remain::unsorted] NoOp => {} + #[remain::unsorted] + Init => { + // Display the menu bar at the top of the window + let message = MenuBarMessage::SendLayout.into(); + queue.push_back(message); + + // Load the default font + let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into()); + let message = FrontendMessage::TriggerFontLoad { font, is_default: true }.into(); + queue.push_back(message); + } + Broadcast(message) => self.message_handlers.broadcast_message_handler.process_action(message, (), &mut queue), Dialog(message) => { self.message_handlers @@ -94,14 +107,16 @@ impl Dispatcher { .process_action(message, &self.message_handlers.portfolio_message_handler, &mut queue); } Frontend(message) => { - // Image and font loading should be immediately handled - if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message { + // Handle these messages immediately by returning early + if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } | FrontendMessage::TriggerRefreshBoundsOfViewports = message { self.responses.push(message); + + // Return early to avoid running the code after the match block return; + } else { + // `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed + self.responses.push(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 queue); @@ -122,15 +137,19 @@ impl Dispatcher { .process_action(message, &self.message_handlers.input_preprocessor_message_handler, &mut 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, - self.message_handlers.portfolio_message_handler.font_cache(), - ), - &mut queue, - ); + if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() { + self.message_handlers.tool_message_handler.process_action( + message, + ( + document, + &self.message_handlers.input_preprocessor_message_handler, + self.message_handlers.portfolio_message_handler.font_cache(), + ), + &mut queue, + ); + } else { + log::warn!("Called ToolMessage without an active document.\nGot {:?}", message); + } } Workspace(message) => { self.message_handlers @@ -153,7 +172,9 @@ impl Dispatcher { 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()); + if self.message_handlers.portfolio_message_handler.active_document().is_some() { + list.extend(self.message_handlers.tool_message_handler.actions()); + } list.extend(self.message_handlers.portfolio_message_handler.actions()); list } @@ -192,6 +213,7 @@ mod test { set_uuid_seed(0); let mut editor = Editor::new(); + editor.new_document(); editor.select_primary_color(Color::RED); editor.draw_rect(100., 200., 300., 400.); editor.select_primary_color(Color::BLUE); @@ -211,14 +233,14 @@ mod test { 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 document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone(); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); editor.handle_message(PortfolioMessage::PasteIntoFolder { clipboard: Clipboard::Internal, folder_path: vec![], insert_index: -1, }); - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); + let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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(); @@ -245,7 +267,7 @@ mod test { 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 document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone(); let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1]; editor.handle_message(DocumentMessage::SetSelectedLayers { @@ -258,7 +280,7 @@ mod test { insert_index: -1, }); - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); + let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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(); @@ -290,7 +312,7 @@ mod test { 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 document_before_added_shapes = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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. @@ -314,7 +336,7 @@ mod test { replacement_selected_layers: vec![vec![folder_id]], }); - let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); + let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone(); editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); editor.handle_message(DocumentMessage::DeleteSelectedLayers); @@ -329,7 +351,7 @@ mod test { insert_index: -1, }); - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); + let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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(); @@ -380,7 +402,7 @@ mod test { 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 document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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]; @@ -401,7 +423,7 @@ mod test { insert_index: -1, }); - let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone(); + let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().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(); @@ -431,7 +453,7 @@ mod test { 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()); + let sorted_layers = map_to_vec(editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().all_layers_sorted()); println!("Sorted layers: {:?}", sorted_layers); let verify_order = |handler: &mut DocumentMessageHandler| { @@ -447,15 +469,15 @@ mod test { }); 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()); + let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); 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()); + let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); 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()); + let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); } diff --git a/editor/src/communication/message.rs b/editor/src/communication/message.rs index d7d2afce98..928a2d38ae 100644 --- a/editor/src/communication/message.rs +++ b/editor/src/communication/message.rs @@ -22,6 +22,8 @@ where pub enum Message { #[remain::unsorted] NoOp, + #[remain::unsorted] + Init, #[child] Broadcast(BroadcastMessage), #[child] diff --git a/editor/src/dialog/dialog_message.rs b/editor/src/dialog/dialog_message.rs index 7066854d66..103023d30f 100644 --- a/editor/src/dialog/dialog_message.rs +++ b/editor/src/dialog/dialog_message.rs @@ -18,7 +18,7 @@ pub enum DialogMessage { // Messages CloseAllDocumentsWithConfirmation, CloseDialogAndThen { - followup: Box, + followups: Vec, }, DisplayDialogError { title: String, diff --git a/editor/src/dialog/dialog_message_handler.rs b/editor/src/dialog/dialog_message_handler.rs index 49b828457d..c2512302a4 100644 --- a/editor/src/dialog/dialog_message_handler.rs +++ b/editor/src/dialog/dialog_message_handler.rs @@ -25,9 +25,11 @@ impl MessageHandler for DialogMessageHa dialog.register_properties(responses, LayoutTarget::DialogDetails); responses.push_back(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }.into()); } - DialogMessage::CloseDialogAndThen { followup } => { + DialogMessage::CloseDialogAndThen { followups } => { responses.push_back(FrontendMessage::DisplayDialogDismiss.into()); - responses.push_back(*followup); + for message in followups.into_iter() { + responses.push_back(message); + } } DialogMessage::DisplayDialogError { title, description } => { let dialog = dialogs::Error { title, description }; @@ -54,36 +56,38 @@ impl MessageHandler for DialogMessageHa responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into()); } DialogMessage::RequestExportDialog => { - let artboard_handler = &portfolio.active_document().artboard_message_handler; - let mut index = 0; - let artboards = artboard_handler - .artboard_ids - .iter() - .rev() - .filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer))) - .map(|(artboard, layer)| { - ( - artboard, - format!( - "Artboard: {}", - layer.name.clone().unwrap_or_else(|| { - index += 1; - format!("Untitled {index}") - }) - ), - ) - }) - .collect(); + if let Some(document) = portfolio.active_document() { + let artboard_handler = &document.artboard_message_handler; + let mut index = 0; + let artboards = artboard_handler + .artboard_ids + .iter() + .rev() + .filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer))) + .map(|(artboard, layer)| { + ( + artboard, + format!( + "Artboard: {}", + layer.name.clone().unwrap_or_else(|| { + index += 1; + format!("Untitled {index}") + }) + ), + ) + }) + .collect(); - self.export_dialog = Export { - file_name: portfolio.active_document().name.clone(), - scale_factor: 1., - artboards, - has_selection: portfolio.active_document().selected_layers().next().is_some(), - ..Default::default() - }; - self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails); - responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into()); + self.export_dialog = Export { + file_name: document.name.clone(), + scale_factor: 1., + artboards, + has_selection: document.selected_layers().next().is_some(), + ..Default::default() + }; + self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails); + responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into()); + } } DialogMessage::RequestNewDocumentDialog => { self.new_document_dialog = NewDocument { @@ -97,5 +101,9 @@ impl MessageHandler for DialogMessageHa } } - advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,RequestExportDialog,CloseAllDocumentsWithConfirmation); + advertise_actions!(DialogMessageDiscriminant; + RequestNewDocumentDialog, + RequestExportDialog, + CloseAllDocumentsWithConfirmation, + ); } diff --git a/editor/src/dialog/dialogs/close_all_documents_dialog.rs b/editor/src/dialog/dialogs/close_all_documents_dialog.rs index 98845e4608..917b3cc74a 100644 --- a/editor/src/dialog/dialogs/close_all_documents_dialog.rs +++ b/editor/src/dialog/dialogs/close_all_documents_dialog.rs @@ -12,7 +12,7 @@ impl PropertyHolder for CloseAllDocuments { min_width: 96, on_update: WidgetCallback::new(|_| { DialogMessage::CloseDialogAndThen { - followup: Box::new(PortfolioMessage::CloseAllDocuments.into()), + followups: vec![PortfolioMessage::CloseAllDocuments.into()], } .into() }), diff --git a/editor/src/dialog/dialogs/close_document_dialog.rs b/editor/src/dialog/dialogs/close_document_dialog.rs index d9df3a4489..2d54520c9a 100644 --- a/editor/src/dialog/dialogs/close_document_dialog.rs +++ b/editor/src/dialog/dialogs/close_document_dialog.rs @@ -1,5 +1,5 @@ use crate::layout::widgets::*; -use crate::message_prelude::{DialogMessage, DocumentMessage, FrontendMessage, PortfolioMessage}; +use crate::message_prelude::*; /// A dialog for confirming the closing a document with unsaved changes. pub struct CloseDocument { @@ -18,7 +18,7 @@ impl PropertyHolder for CloseDocument { emphasized: true, on_update: WidgetCallback::new(|_| { DialogMessage::CloseDialogAndThen { - followup: Box::new(DocumentMessage::SaveDocument.into()), + followups: vec![DocumentMessage::SaveDocument.into()], } .into() }), @@ -29,7 +29,7 @@ impl PropertyHolder for CloseDocument { min_width: 96, on_update: WidgetCallback::new(move |_| { DialogMessage::CloseDialogAndThen { - followup: Box::new(PortfolioMessage::CloseDocument { document_id }.into()), + followups: vec![BroadcastSignal::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()], } .into() }), diff --git a/editor/src/dialog/dialogs/export_dialog.rs b/editor/src/dialog/dialogs/export_dialog.rs index 2aa8f6ced8..657eabc5c1 100644 --- a/editor/src/dialog/dialogs/export_dialog.rs +++ b/editor/src/dialog/dialogs/export_dialog.rs @@ -33,6 +33,7 @@ impl PropertyHolder for Export { WidgetHolder::new(Widget::TextInput(TextInput { value: self.file_name.clone(), on_update: WidgetCallback::new(|text_input: &TextInput| ExportDialogUpdate::FileName(text_input.value.clone()).into()), + ..Default::default() })), ]; @@ -123,7 +124,7 @@ impl PropertyHolder for Export { emphasized: true, on_update: WidgetCallback::new(|_| { DialogMessage::CloseDialogAndThen { - followup: Box::new(ExportDialogUpdate::Submit.into()), + followups: vec![ExportDialogUpdate::Submit.into()], } .into() }), diff --git a/editor/src/dialog/dialogs/new_document_dialog.rs b/editor/src/dialog/dialogs/new_document_dialog.rs index 425a4d9f4e..ecc7f6072f 100644 --- a/editor/src/dialog/dialogs/new_document_dialog.rs +++ b/editor/src/dialog/dialogs/new_document_dialog.rs @@ -34,6 +34,7 @@ impl PropertyHolder for NewDocument { WidgetHolder::new(Widget::TextInput(TextInput { value: self.name.clone(), on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogUpdate::Name(text_input.value.clone()).into()), + ..Default::default() })), ]; @@ -98,7 +99,7 @@ impl PropertyHolder for NewDocument { emphasized: true, on_update: WidgetCallback::new(|_| { DialogMessage::CloseDialogAndThen { - followup: Box::new(NewDocumentDialogUpdate::Submit.into()), + followups: vec![NewDocumentDialogUpdate::Submit.into()], } .into() }), @@ -131,9 +132,6 @@ pub enum NewDocumentDialogUpdate { DimensionsY(f64), Submit, - BufferArtboard, - AddArtboard, - FitCanvas, } impl MessageHandler for NewDocument { @@ -147,27 +145,18 @@ impl MessageHandler for NewDocument { NewDocumentDialogUpdate::Submit => { responses.push_back(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }.into()); - responses.push_back(NewDocumentDialogUpdate::BufferArtboard.into()); - } - NewDocumentDialogUpdate::BufferArtboard => { - if !self.infinite { - responses.push_back(NewDocumentDialogUpdate::AddArtboard.into()); + if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 { + responses.push_back( + ArtboardMessage::AddArtboard { + id: None, + position: (0., 0.), + size: (self.dimensions.x as f64, self.dimensions.y as f64), + } + .into(), + ); + responses.push_back(DocumentMessage::ZoomCanvasToFitAll.into()); } } - NewDocumentDialogUpdate::AddArtboard => { - responses.push_back( - ArtboardMessage::AddArtboard { - id: None, - position: (0., 0.), - size: (self.dimensions.x as f64, self.dimensions.y as f64), - } - .into(), - ); - responses.push_back(NewDocumentDialogUpdate::FitCanvas.into()); - } - NewDocumentDialogUpdate::FitCanvas => { - responses.push_back(DocumentMessage::ZoomCanvasToFitAll.into()); - } } self.register_properties(responses, LayoutTarget::DialogDetails); diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 7501ea4c4a..9e176a9a21 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -552,8 +552,9 @@ impl DocumentMessageHandler { on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()), })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Snapping".into(), + header: "Snapping".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Unrelated, @@ -566,8 +567,9 @@ impl DocumentMessageHandler { on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()), })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Grid".into(), + header: "Grid".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Unrelated, @@ -580,8 +582,9 @@ impl DocumentMessageHandler { on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()), })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Overlays".into(), + header: "Overlays".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Unrelated, @@ -614,8 +617,9 @@ impl DocumentMessageHandler { ], })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "View Mode".into(), + header: "View Mode".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { separator_type: SeparatorType::Section, diff --git a/editor/src/document/movement_message_handler.rs b/editor/src/document/movement_message_handler.rs index 93ba9b4a05..7811fcf00c 100644 --- a/editor/src/document/movement_message_handler.rs +++ b/editor/src/document/movement_message_handler.rs @@ -221,6 +221,7 @@ impl MessageHandler { diff --git a/editor/src/document/overlays_message_handler.rs b/editor/src/document/overlays_message_handler.rs index b0a33d61c5..a6f12fd406 100644 --- a/editor/src/document/overlays_message_handler.rs +++ b/editor/src/document/overlays_message_handler.rs @@ -45,6 +45,8 @@ impl MessageHandler ActionList { - actions!(OverlaysMessageDiscriminant; ClearAllOverlays) + actions!(OverlaysMessageDiscriminant; + ClearAllOverlays, + ) } } diff --git a/editor/src/document/portfolio_message.rs b/editor/src/document/portfolio_message.rs index 5cf987cf48..d51268d867 100644 --- a/editor/src/document/portfolio_message.rs +++ b/editor/src/document/portfolio_message.rs @@ -37,6 +37,7 @@ pub enum PortfolioMessage { Cut { clipboard: Clipboard, }, + DestroyAllDocuments, FontLoaded { font_family: String, font_style: String, @@ -48,7 +49,6 @@ pub enum PortfolioMessage { font: Font, is_default: bool, }, - NewDocument, NewDocumentWithName { name: String, }, diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index 4ef4ad879c..f21b636071 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -15,23 +15,23 @@ use graphene::Operation as DocumentOperation; use log::warn; use std::collections::{HashMap, VecDeque}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PortfolioMessageHandler { menu_bar_message_handler: MenuBarMessageHandler, documents: HashMap, document_ids: Vec, - active_document_id: u64, + active_document_id: Option, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], font_cache: FontCache, } impl PortfolioMessageHandler { - pub fn active_document(&self) -> &DocumentMessageHandler { - self.documents.get(&self.active_document_id).unwrap() + pub fn active_document(&self) -> Option<&DocumentMessageHandler> { + self.active_document_id.and_then(|id| self.documents.get(&id)) } - pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler { - self.documents.get_mut(&self.active_document_id).unwrap() + pub fn active_document_mut(&mut self) -> Option<&mut DocumentMessageHandler> { + self.active_document_id.and_then(|id| self.documents.get_mut(&id)) } pub fn generate_new_document_name(&self) -> String { @@ -56,21 +56,8 @@ impl PortfolioMessageHandler { } // TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load* - fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque) { - // Special case when loading a document on an empty page - if replace_first_empty && self.active_document().is_unmodified_default() { - responses.push_back(BroadcastSignal::ToolAbort.into()); - responses.push_back(PortfolioMessage::CloseDocument { document_id: self.active_document_id }.into()); - - let active_document_index = self - .document_ids - .iter() - .position(|id| self.active_document_id == *id) - .expect("Did not find matching active document id"); - self.document_ids.insert(active_document_index + 1, document_id); - } else { - self.document_ids.push(document_id); - } + fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, responses: &mut VecDeque) { + self.document_ids.push(document_id); responses.extend( new_document @@ -86,9 +73,19 @@ impl PortfolioMessageHandler { self.documents.insert(document_id, new_document); - responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); + if self.active_document().is_some() { + responses.push_back(PropertiesPanelMessage::Deactivate.into()); + responses.push_back(BroadcastSignal::ToolAbort.into()); + responses.push_back(ToolMessage::DeactivateTools.into()); + } + responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); responses.push_back(PortfolioMessage::SelectDocument { document_id }.into()); + responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); + responses.push_back(ToolMessage::InitTools.into()); + responses.push_back(PropertiesPanelMessage::Init.into()); + responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); + responses.push_back(DocumentMessage::DocumentStructureChanged.into()) } /// Returns an iterator over the open documents in order. @@ -105,25 +102,6 @@ impl PortfolioMessageHandler { } } -impl Default for PortfolioMessageHandler { - fn default() -> Self { - let mut documents_map: HashMap = HashMap::with_capacity(1); - let starting_key = generate_uuid(); - documents_map.insert(starting_key, DocumentMessageHandler::default()); - - const EMPTY_VEC: Vec = vec![]; - - Self { - documents: documents_map, - document_ids: vec![starting_key], - copy_buffer: [EMPTY_VEC; INTERNAL_CLIPBOARD_COUNT as usize], - active_document_id: starting_key, - font_cache: Default::default(), - menu_bar_message_handler: MenuBarMessageHandler::default(), - } - } -} - impl MessageHandler for PortfolioMessageHandler { #[remain::check] fn process_action(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { @@ -134,12 +112,20 @@ impl MessageHandler for Port match message { // Sub-messages #[remain::unsorted] - Document(message) => self.documents.get_mut(&self.active_document_id).unwrap().process_action(message, (ipp, &self.font_cache), responses), + Document(message) => { + if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) { + document.process_action(message, (ipp, &self.font_cache), responses) + } + } #[remain::unsorted] MenuBar(message) => self.menu_bar_message_handler.process_action(message, (), responses), // Messages - AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into()), + AutoSaveActiveDocument => { + if let Some(document_id) = self.active_document_id { + responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into()); + } + } AutoSaveDocument { document_id } => { let document = self.documents.get(&document_id).unwrap(); responses.push_back( @@ -156,54 +142,60 @@ impl MessageHandler for Port ) } CloseActiveDocumentWithConfirmation => { - responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id: self.active_document_id }.into()); + if let Some(document_id) = self.active_document_id { + responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into()); + } } CloseAllDocuments => { - // Empty the list of internal document data - self.documents.clear(); - self.document_ids.clear(); + if self.active_document_id.is_some() { + responses.push_back(PropertiesPanelMessage::Deactivate.into()); + responses.push_back(BroadcastSignal::ToolAbort.into()); + responses.push_back(ToolMessage::DeactivateTools.into()); + } - // Clear out all documents and make a new default document - let new_document_id = generate_uuid(); - self.documents.insert(new_document_id, DocumentMessageHandler::default()); - self.document_ids.push(new_document_id); - self.active_document_id = new_document_id; + for document_id in &self.document_ids { + responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id: *document_id }.into()); + } - responses.push_back(BroadcastSignal::ToolAbort.into()); + responses.push_back(PortfolioMessage::DestroyAllDocuments.into()); responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); - responses.push_back(PortfolioMessage::SelectDocument { document_id: new_document_id }.into()) } CloseDocument { document_id } => { let document_index = self.document_index(document_id); self.documents.remove(&document_id); self.document_ids.remove(document_index); - // Last tab was closed, so create a new blank tab if self.document_ids.is_empty() { - let new_id = generate_uuid(); - self.document_ids.push(new_id); - self.documents.insert(new_id, DocumentMessageHandler::default()); + self.active_document_id = None; + } else if Some(document_id) == self.active_document_id { + if document_index == self.document_ids.len() { + // If we closed the last document take the one previous (same as last) + responses.push_back( + PortfolioMessage::SelectDocument { + document_id: *self.document_ids.last().unwrap(), + } + .into(), + ); + } else { + // Move to the next tab + responses.push_back( + PortfolioMessage::SelectDocument { + document_id: self.document_ids[document_index], + } + .into(), + ); + } } - self.active_document_id = if document_id != self.active_document_id { - // If we are not closing the active document, stay on it - self.active_document_id - } else if document_index >= self.document_ids.len() { - // If we closed the last document take the one previous (same as last) - *self.document_ids.last().unwrap() - } else { - // Move to the next tab - self.document_ids[document_index] - }; - // Send the new list of document tab names responses.push_back(UpdateOpenDocumentsList.into()); - responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: self.active_document_id }.into()); responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_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 { affected_layer_path: layer.clone() }.into()); + if let Some(document) = self.active_document() { + for layer in document.layer_metadata.keys() { + responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into()); + } } } CloseDocumentWithConfirmation { document_id } => { @@ -225,36 +217,42 @@ impl MessageHandler for Port } 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_val = |buffer: &mut Vec| { - 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) => { - buffer.push(CopyBufferEntry { layer, layer_metadata }); + if let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get(&id)) { + let copy_val = |buffer: &mut Vec| { + 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) => { + buffer.push(CopyBufferEntry { layer, layer_metadata }); + } + (Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e), } - (Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e), } - } - }; - - if clipboard == Clipboard::Device { - let mut buffer = Vec::new(); - copy_val(&mut buffer); - let mut copy_text = String::from("graphite/layer: "); - copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); + }; - responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into()); - } else { - let copy_buffer = &mut self.copy_buffer; - copy_buffer[clipboard as usize].clear(); - copy_val(&mut copy_buffer[clipboard as usize]); + if clipboard == Clipboard::Device { + let mut buffer = Vec::new(); + copy_val(&mut buffer); + let mut copy_text = String::from("graphite/layer: "); + copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); + + responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into()); + } else { + let copy_buffer = &mut self.copy_buffer; + copy_buffer[clipboard as usize].clear(); + copy_val(&mut copy_buffer[clipboard as usize]); + } } } Cut { clipboard } => { responses.push_back(Copy { clipboard }.into()); responses.push_back(DeleteSelectedLayers.into()); } + DestroyAllDocuments => { + // Empty the list of internal document data + self.documents.clear(); + self.document_ids.clear(); + self.active_document_id = None; + } FontLoaded { font_family, font_style, @@ -263,33 +261,35 @@ impl MessageHandler for Port is_default, } => { self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default); - self.active_document_mut().graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text); - responses.push_back(DocumentMessage::RenderDocument.into()); + + if let Some(document) = self.active_document_mut() { + document.graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text); + responses.push_back(DocumentMessage::RenderDocument.into()); + } } LoadFont { font, is_default } => { if !self.font_cache.loaded_font(&font) { responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into()); } } - NewDocument => { - let name = self.generate_new_document_name(); - let new_document = DocumentMessageHandler::with_name(name, ipp); - let document_id = generate_uuid(); - responses.push_back(BroadcastSignal::ToolAbort.into()); - self.load_document(new_document, document_id, false, responses); - } NewDocumentWithName { name } => { let new_document = DocumentMessageHandler::with_name(name, ipp); let document_id = generate_uuid(); - responses.push_back(BroadcastSignal::ToolAbort.into()); - self.load_document(new_document, document_id, false, responses); + if self.active_document().is_some() { + responses.push_back(BroadcastSignal::ToolAbort.into()); + responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); + } + + self.load_document(new_document, document_id, 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]; + if let Some(active_document_id) = self.active_document_id { + let current_index = self.document_index(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 { document_id: next_id }.into()); + responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into()); + } } OpenDocument => { responses.push_back(FrontendMessage::TriggerFileUpload.into()); @@ -318,7 +318,7 @@ impl MessageHandler for Port match document { Ok(mut document) => { document.set_save_state(document_is_saved); - self.load_document(document, document_id, true, responses); + self.load_document(document, document_id, responses); } Err(e) => responses.push_back( DialogMessage::DisplayDialogError { @@ -330,22 +330,26 @@ impl MessageHandler for Port } } Paste { clipboard } => { - let document = self.active_document(); - let shallowest_common_folder = document - .graphene_document - .shallowest_common_folder(document.selected_layers()) - .expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); - responses.push_back(DeselectAllLayers.into()); - responses.push_back(StartTransaction.into()); - responses.push_back( - PasteIntoFolder { - clipboard, - folder_path: shallowest_common_folder.to_vec(), - insert_index: -1, - } - .into(), - ); - responses.push_back(CommitTransaction.into()); + let shallowest_common_folder = self.active_document().map(|document| { + document + .graphene_document + .shallowest_common_folder(document.selected_layers()) + .expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion") + }); + + if let Some(folder) = shallowest_common_folder { + responses.push_back(DeselectAllLayers.into()); + responses.push_back(StartTransaction.into()); + responses.push_back( + PasteIntoFolder { + clipboard, + folder_path: folder.to_vec(), + insert_index: -1, + } + .into(), + ); + responses.push_back(CommitTransaction.into()); + } } PasteIntoFolder { clipboard, @@ -353,50 +357,9 @@ impl MessageHandler for Port insert_index, } => { let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| { - log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index); - - let destination_path = [path.to_vec(), vec![generate_uuid()]].concat(); - - responses.push_front( - DocumentMessage::UpdateLayerMetadata { - layer_path: destination_path.clone(), - layer_metadata: entry.layer_metadata, - } - .into(), - ); - self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone()); - responses.push_front( - DocumentOperation::InsertLayer { - layer: entry.layer.clone(), - destination_path, - insert_index, - } - .into(), - ); - }; - - if insert_index == -1 { - for entry in self.copy_buffer[clipboard as usize].iter().rev() { - paste(entry, responses) - } - } else { - for entry in self.copy_buffer[clipboard as usize].iter() { - paste(entry, responses) - } - } - } - PasteSerializedData { data } => { - if let Ok(data) = serde_json::from_str::>(&data) { - let document = self.active_document(); - let shallowest_common_folder = document - .graphene_document - .shallowest_common_folder(document.selected_layers()) - .expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); - responses.push_back(DeselectAllLayers.into()); - responses.push_back(StartTransaction.into()); - - for entry in data.iter().rev() { - let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat(); + if let Some(document) = self.active_document() { + log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index); + let destination_path = [path.to_vec(), vec![generate_uuid()]].concat(); responses.push_front( DocumentMessage::UpdateLayerMetadata { @@ -405,36 +368,92 @@ impl MessageHandler for Port } .into(), ); - self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone()); + document.load_layer_resources(responses, &entry.layer.data, destination_path.clone()); responses.push_front( DocumentOperation::InsertLayer { layer: entry.layer.clone(), destination_path, - insert_index: -1, + insert_index, } .into(), ); } + }; - responses.push_back(CommitTransaction.into()); + if insert_index == -1 { + for entry in self.copy_buffer[clipboard as usize].iter().rev() { + paste(entry, responses) + } + } else { + for entry in self.copy_buffer[clipboard as usize].iter() { + paste(entry, responses) + } + } + } + PasteSerializedData { data } => { + if let Some(document) = self.active_document() { + if let Ok(data) = serde_json::from_str::>(&data) { + let shallowest_common_folder = document + .graphene_document + .shallowest_common_folder(document.selected_layers()) + .expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); + responses.push_back(DeselectAllLayers.into()); + responses.push_back(StartTransaction.into()); + + for entry in data.iter().rev() { + let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat(); + + responses.push_front( + DocumentMessage::UpdateLayerMetadata { + layer_path: destination_path.clone(), + layer_metadata: entry.layer_metadata, + } + .into(), + ); + document.load_layer_resources(responses, &entry.layer.data, destination_path.clone()); + responses.push_front( + DocumentOperation::InsertLayer { + layer: entry.layer.clone(), + destination_path, + insert_index: -1, + } + .into(), + ); + } + + responses.push_back(CommitTransaction.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 { document_id: prev_id }.into()); + if let Some(active_document_id) = self.active_document_id { + let len = self.document_ids.len(); + let current_index = self.document_index(active_document_id); + let prev_index = (current_index + len - 1) % len; + let prev_id = self.document_ids[prev_index]; + responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into()); + } } - SelectDocument { document_id } => { - let active_document = self.active_document(); - if !active_document.is_saved() { - responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into()); + if let Some(document) = self.active_document() { + if !document.is_saved() { + // Safe to unwrap since we know that there is an active document + responses.push_back( + PortfolioMessage::AutoSaveDocument { + document_id: self.active_document_id.unwrap(), + } + .into(), + ); + } + } + + if self.active_document().is_some() { + responses.push_back(BroadcastSignal::ToolAbort.into()); } - responses.push_back(BroadcastSignal::ToolAbort.into()); - responses.push_back(SetActiveDocument { document_id }.into()); + // TODO: Remove this message in favor of having tools have specific data per document instance + responses.push_back(SetActiveDocument { document_id }.into()); + responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into()); responses.push_back(RenderDocument.into()); responses.push_back(DocumentMessage::DocumentStructureChanged.into()); @@ -444,13 +463,13 @@ impl MessageHandler for Port responses.push_back(BroadcastSignal::SelectionChanged.into()); responses.push_back(BroadcastSignal::DocumentIsDirty.into()); responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into()); + responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); } - SetActiveDocument { document_id } => { - self.active_document_id = document_id; - } + SetActiveDocument { document_id } => self.active_document_id = Some(document_id), UpdateDocumentWidgets => { - let active_document = self.active_document(); - active_document.update_document_widgets(responses); + if let Some(document) = self.active_document() { + document.update_document_widgets(responses); + } } UpdateOpenDocumentsList => { // Send the list of document tab names @@ -472,7 +491,6 @@ impl MessageHandler for Port fn actions(&self) -> ActionList { let mut common = actions!(PortfolioMessageDiscriminant; - NewDocument, CloseActiveDocumentWithConfirmation, CloseAllDocuments, NextDocument, @@ -481,14 +499,17 @@ impl MessageHandler for Port Paste, ); - if self.active_document().layer_metadata.values().any(|data| data.selected) { - let select = actions!(PortfolioMessageDiscriminant; - Copy, - Cut, - ); - common.extend(select); + if let Some(document) = self.active_document() { + if document.layer_metadata.values().any(|data| data.selected) { + let select = actions!(PortfolioMessageDiscriminant; + Copy, + Cut, + ); + common.extend(select); + } + common.extend(document.actions()); } - common.extend(self.active_document().actions()); + common } } diff --git a/editor/src/document/properties_panel_message.rs b/editor/src/document/properties_panel_message.rs index 5eae8fd352..9748478acf 100644 --- a/editor/src/document/properties_panel_message.rs +++ b/editor/src/document/properties_panel_message.rs @@ -12,6 +12,7 @@ pub enum PropertiesPanelMessage { CheckSelectedWasDeleted { path: Vec }, CheckSelectedWasUpdated { path: Vec }, ClearSelection, + Deactivate, Init, ModifyFill { fill: Fill }, ModifyFont { font_family: String, font_style: String, size: f64 }, diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index 88e6853bee..aac4016522 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -2,8 +2,8 @@ use super::utility_types::TargetDocument; use crate::document::properties_panel_message::TransformOp; use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::{ - ColorInput, FontInput, IconLabel, Layout, LayoutGroup, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextAreaInput, TextInput, TextLabel, - Widget, WidgetCallback, WidgetHolder, WidgetLayout, + ColorInput, FontInput, IconLabel, IconStyle, Layout, LayoutGroup, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextAreaInput, TextInput, + TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout, }; use crate::message_prelude::*; @@ -156,6 +156,13 @@ impl<'a> MessageHandler responses.push_back( + BroadcastMessage::UnsubscribeSignal { + on: BroadcastSignal::SelectionChanged, + message: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()), + } + .into(), + ), Init => responses.push_back( BroadcastMessage::SubscribeSignal { on: BroadcastSignal::SelectionChanged, @@ -262,7 +269,7 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "NodeFolder".into(), - gap_after: true, + icon_style: IconStyle::Node, })), LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "NodeShape".into(), - gap_after: true, + icon_style: IconStyle::Node, })), LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "NodeText".into(), - gap_after: true, + icon_style: IconStyle::Node, })), LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "NodeImage".into(), - gap_after: true, + icon_style: IconStyle::Node, })), }, WidgetHolder::new(Widget::Separator(Separator { @@ -474,14 +484,16 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque LayoutGroup { WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { value: layer.text.clone(), on_update: WidgetCallback::new(|text_area: &TextAreaInput| PropertiesPanelMessage::ModifyText { new_text: text_area.value.clone() }.into()), + ..Default::default() })), ], }, @@ -729,6 +742,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { } .into() }), + ..Default::default() })), ], }, @@ -754,6 +768,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup { } .into() }), + ..Default::default() })), ], }, @@ -997,6 +1012,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup { .with_dash_lengths(&text_input.value) .map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into()) }), + ..Default::default() })), ], }, diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index ca0597bf0d..eee20af828 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -17,7 +17,7 @@ pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog DisplayDialog { icon: String }, DisplayDialogDismiss, - DisplayDialogPanic { panic_info: String, title: String, description: String }, + DisplayDialogPanic { panic_info: String, header: String, description: String }, DisplayEditableTextbox { text: String, line_width: Option, font_size: f64, color: Color }, DisplayRemoveEditableTextbox, @@ -30,6 +30,7 @@ pub enum FrontendMessage { TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, TriggerPaste, TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) }, + TriggerRefreshBoundsOfViewports, TriggerTextCommit, TriggerTextCopy { copy_text: String }, TriggerViewportResize, @@ -58,5 +59,5 @@ pub enum FrontendMessage { UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateToolShelfLayout { layout_target: LayoutTarget, layout: SubLayout }, - UpdateWorkingColors { primary: Color, secondary: Color }, + UpdateWorkingColorsLayout { layout_target: LayoutTarget, layout: SubLayout }, } diff --git a/editor/src/global/global_message_handler.rs b/editor/src/global/global_message_handler.rs index 6e6b88df43..6f0a4df1d9 100644 --- a/editor/src/global/global_message_handler.rs +++ b/editor/src/global/global_message_handler.rs @@ -27,5 +27,9 @@ impl MessageHandler for GlobalMessageHandler { } } - advertise_actions!(GlobalMessageDiscriminant; LogInfo, LogDebug, LogTrace); + advertise_actions!(GlobalMessageDiscriminant; + LogInfo, + LogDebug, + LogTrace, + ); } diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index d421cc1547..d0d3dc64cb 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -266,14 +266,12 @@ impl Default for Mapping { impl Mapping { pub fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option { - use InputMapperMessage::*; - let list = match message { - KeyDown(key) => &self.key_down[key as usize], - KeyUp(key) => &self.key_up[key as usize], - DoubleClick => &self.double_click, - MouseScroll => &self.mouse_scroll, - PointerMove => &self.pointer_move, + InputMapperMessage::KeyDown(key) => &self.key_down[key as usize], + InputMapperMessage::KeyUp(key) => &self.key_up[key as usize], + InputMapperMessage::DoubleClick => &self.double_click, + InputMapperMessage::MouseScroll => &self.mouse_scroll, + InputMapperMessage::PointerMove => &self.pointer_move, }; list.match_mapping(keys, actions) } diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index d5bd6c19dc..da03f3715c 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -156,7 +156,7 @@ impl InputPreprocessorMessageHandler { } pub fn document_bounds(&self) -> [DVec2; 2] { - // ipp bounds are relative to the entire screen + // IPP bounds are relative to the entire application [(0., 0.).into(), self.viewport_bounds.bottom_right - self.viewport_bounds.top_left] } } diff --git a/editor/src/layout/layout_message.rs b/editor/src/layout/layout_message.rs index c03fd70c22..1555036e78 100644 --- a/editor/src/layout/layout_message.rs +++ b/editor/src/layout/layout_message.rs @@ -25,6 +25,7 @@ pub enum LayoutTarget { PropertiesSections, ToolOptions, ToolShelf, + WorkingColors, // KEEP THIS ENUM LAST // This is a marker that is used to define an array that is used to hold widgets diff --git a/editor/src/layout/layout_message_handler.rs b/editor/src/layout/layout_message_handler.rs index f900752ed9..e633c48c32 100644 --- a/editor/src/layout/layout_message_handler.rs +++ b/editor/src/layout/layout_message_handler.rs @@ -55,6 +55,10 @@ impl LayoutMessageHandler { layout_target, layout: layout.clone().unwrap_widget_layout().layout, }, + LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { + layout_target, + layout: layout.clone().unwrap_widget_layout().layout, + }, #[remain::unsorted] LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), @@ -168,6 +172,7 @@ impl MessageHandler for LayoutMessageHandler { responses.push_back(callback_message); } Widget::Separator(_) => {} + Widget::SwatchPairInput(_) => {} Widget::TextAreaInput(text_area_input) => { let update_value = value.as_str().expect("TextAreaInput update was not of type: string"); text_area_input.value = update_value.into(); diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index cced182671..36bed1cb61 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -1,6 +1,7 @@ use super::layout_message::LayoutTarget; use crate::input::keyboard::Key; use crate::message_prelude::*; +use crate::Color; use derivative::*; use serde::{Deserialize, Serialize}; @@ -195,18 +196,18 @@ pub type SubLayout = Vec; #[remain::sorted] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum LayoutGroup { + #[serde(rename = "column")] Column { #[serde(rename = "columnWidgets")] widgets: Vec, }, + #[serde(rename = "row")] Row { #[serde(rename = "rowWidgets")] widgets: Vec, }, - Section { - name: String, - layout: SubLayout, - }, + #[serde(rename = "section")] + Section { name: String, layout: SubLayout }, } #[derive(Debug, Default)] @@ -281,6 +282,7 @@ impl<'a> Iterator for WidgetIterMut<'a> { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct WidgetHolder { + #[serde(rename = "widgetId")] pub widget_id: u64, pub widget: Widget, } @@ -323,260 +325,357 @@ pub enum Widget { PopoverButton(PopoverButton), RadioInput(RadioInput), Separator(Separator), + SwatchPairInput(SwatchPairInput), TextAreaInput(TextAreaInput), TextButton(TextButton), TextInput(TextInput), TextLabel(TextLabel), } -#[derive(Clone, Serialize, Deserialize, Derivative)] -#[derivative(Debug, PartialEq, Default)] -pub struct NumberInput { - pub value: Option, - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, - pub min: Option, - pub max: Option, - #[serde(rename = "isInteger")] - pub is_integer: bool, - #[serde(rename = "incrementBehavior")] - pub increment_behavior: NumberInputIncrementBehavior, - #[serde(rename = "incrementFactor")] - #[derivative(Default(value = "1."))] - pub increment_factor: f64, - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub increment_callback_increase: WidgetCallback, +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct CheckboxInput { + pub checked: bool, + + pub icon: String, + + pub tooltip: String, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub increment_callback_decrease: WidgetCallback, - pub label: String, - pub unit: String, - #[serde(rename = "displayDecimalPlaces")] - #[derivative(Default(value = "3"))] - pub display_decimal_places: u32, - pub disabled: bool, + pub on_update: WidgetCallback, } #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] -pub struct TextInput { - pub value: String, +pub struct ColorInput { + pub value: Option, + + pub label: Option, + + #[serde(rename = "noTransparency")] + #[derivative(Default(value = "true"))] + pub no_transparency: bool, + + pub disabled: bool, + + pub tooltip: String, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub on_update: WidgetCallback, } #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] -pub struct TextAreaInput { - pub value: String, - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, +pub struct DropdownInput { + pub entries: DropdownInputEntries, + + // This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI) + #[serde(rename = "selectedIndex")] + pub selected_index: Option, + + #[serde(rename = "drawIcon")] + pub draw_icon: bool, + + #[derivative(Default(value = "true"))] + pub interactive: bool, + + pub disabled: bool, + // + // Callbacks + // `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput` } -#[derive(Clone, Serialize, Deserialize, Derivative)] -#[derivative(Debug, PartialEq, Default)] -pub struct ColorInput { - pub value: Option, +pub type DropdownInputEntries = Vec>; + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct DropdownEntryData { + pub value: String, + + pub label: String, + + pub icon: String, + + pub shortcut: Vec, + + #[serde(rename = "shortcutRequiresLock")] + pub shortcut_requires_lock: bool, + + pub disabled: bool, + + pub children: DropdownInputEntries, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, - #[serde(rename = "canSetTransparent")] - #[derivative(Default(value = "true"))] - pub can_set_transparent: bool, + pub on_update: WidgetCallback<()>, } #[derive(Clone, Serialize, Deserialize, Derivative)] #[derivative(Debug, PartialEq, Default)] pub struct FontInput { - #[serde(rename = "isStyle")] - pub is_style_picker: bool, #[serde(rename = "fontFamily")] pub font_family: String, + #[serde(rename = "fontStyle")] pub font_style: String, + + #[serde(rename = "isStyle")] + pub is_style_picker: bool, + + pub disabled: bool, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub enum NumberInputIncrementBehavior { - Add, - Multiply, - Callback, -} +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct IconButton { + pub icon: String, -impl Default for NumberInputIncrementBehavior { - fn default() -> Self { - Self::Add - } -} + pub size: u32, // TODO: Convert to an `IconSize` enum -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Separator { - pub direction: SeparatorDirection, + pub active: bool, - #[serde(rename = "type")] - pub separator_type: SeparatorType, + pub tooltip: String, + + // Callbacks + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum SeparatorDirection { - Horizontal, - Vertical, +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)] +pub struct IconLabel { + pub icon: String, + + #[serde(rename = "iconStyle")] + pub icon_style: IconStyle, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum SeparatorType { - Related, - Unrelated, - Section, - List, +#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)] +pub enum IconStyle { + #[default] + Normal, + Node, } +/// This widget allows for the flexible use of the layout system. +/// In a custom layout, one can define a widget that is just used to trigger code on the backend. +/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend. #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] -pub struct IconButton { - pub icon: String, - #[serde(rename = "title")] - pub tooltip: String, - pub size: u32, - pub active: bool, - #[serde(rename = "gapAfter")] - pub gap_after: bool, +pub struct Invisible { #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub on_update: WidgetCallback<()>, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] -#[derivative(Debug, PartialEq)] -#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] -pub struct TextButton { +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct NumberInput { pub label: String, - pub emphasized: bool, - pub min_width: u32, - pub gap_after: bool, + + pub value: Option, + + pub min: Option, + + pub max: Option, + + #[serde(rename = "isInteger")] + pub is_integer: bool, + + #[serde(rename = "displayDecimalPlaces")] + #[derivative(Default(value = "3"))] + pub display_decimal_places: u32, + + pub unit: String, + + #[serde(rename = "unitIsHiddenWhenEditing")] + #[derivative(Default(value = "true"))] + pub unit_is_hidden_when_editing: bool, + + #[serde(rename = "incrementBehavior")] + pub increment_behavior: NumberInputIncrementBehavior, + + #[serde(rename = "incrementFactor")] + #[derivative(Default(value = "1."))] + pub increment_factor: f64, + + pub disabled: bool, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, - pub disabled: bool, -} + pub on_update: WidgetCallback, -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] -#[derivative(Debug, PartialEq)] -pub struct OptionalInput { - pub checked: bool, - pub icon: String, - #[serde(rename = "title")] - pub tooltip: String, #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub increment_callback_increase: WidgetCallback, + + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub increment_callback_decrease: WidgetCallback, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)] +pub enum NumberInputIncrementBehavior { + #[default] + Add, + Multiply, + Callback, } #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] -pub struct CheckboxInput { +pub struct OptionalInput { pub checked: bool, + pub icon: String, - #[serde(rename = "title")] + pub tooltip: String, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] -#[derivative(Debug, PartialEq)] +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] pub struct PopoverButton { - pub title: String, + pub icon: Option, + + // Body + pub header: String, + pub text: String, } -#[derive(Clone, Serialize, Deserialize, Derivative)] -#[derivative(Debug, PartialEq, Default)] -pub struct DropdownInput { - pub entries: Vec>, - // This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace with usize when we switch to a native UI) +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct RadioInput { + pub entries: Vec, + + // This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI) #[serde(rename = "selectedIndex")] - pub selected_index: Option, - #[serde(rename = "drawIcon")] - pub draw_icon: bool, - #[derivative(Default(value = "true"))] - pub interactive: bool, - // `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput` - pub disabled: bool, + pub selected_index: u32, } #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] -pub struct DropdownEntryData { +pub struct RadioEntryData { pub value: String, + pub label: String, + pub icon: String, - pub shortcut: Vec, - #[serde(rename = "shortcutRequiresLock")] - pub shortcut_requires_lock: bool, - pub disabled: bool, - pub children: Vec>, + pub tooltip: String, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback<()>, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] -#[derivative(Debug, PartialEq)] -pub struct RadioInput { - pub entries: Vec, +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Separator { + pub direction: SeparatorDirection, - // This uses `u32` instead of `usize` since it will be serialized as a normal JS number - // TODO(mfish33): Replace with usize when using native UI - #[serde(rename = "selectedIndex")] - pub selected_index: u32, + #[serde(rename = "type")] + pub separator_type: SeparatorType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SeparatorDirection { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SeparatorType { + Related, + Unrelated, + Section, + List, +} + +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct SwatchPairInput { + pub primary: Color, + + pub secondary: Color, +} + +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct TextAreaInput { + pub value: String, + + pub label: Option, + + pub disabled: bool, + + // Callbacks + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, } #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] -pub struct RadioEntryData { - pub value: String, +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] +pub struct TextButton { pub label: String, - pub icon: String, - pub tooltip: String, + + pub emphasized: bool, + + #[serde(rename = "minWidth")] + pub min_width: u32, + + pub disabled: bool, + + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback<()>, + pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq)] -pub struct IconLabel { - pub icon: String, - #[serde(rename = "gapAfter")] - pub gap_after: bool, +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct TextInput { + pub value: String, + + pub label: Option, + + pub disabled: bool, + + // Callbacks + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, } #[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)] pub struct TextLabel { - pub value: String, pub bold: bool, + pub italic: bool, - pub multiline: bool, + #[serde(rename = "tableAlign")] pub table_align: bool, -} -// This widget allows for the flexible use of the layout system -// In a custom layout one can define a widget that is just used to trigger code on the backend -// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] -#[derivative(Debug, PartialEq)] -pub struct Invisible { - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback<()>, + pub multiline: bool, + + // Body + pub value: String, } diff --git a/editor/src/misc/macros.rs b/editor/src/misc/macros.rs index 702efffcff..221c149ced 100644 --- a/editor/src/misc/macros.rs +++ b/editor/src/misc/macros.rs @@ -13,7 +13,10 @@ /// /// and /// ```ignore -/// actions!(DocumentMessage; Undo, Redo); +/// actions!(DocumentMessage; +/// Undo, +/// Redo, +/// ); /// ``` /// /// expands to: diff --git a/editor/src/misc/test_utils.rs b/editor/src/misc/test_utils.rs index acaba7e5db..f543e8d281 100644 --- a/editor/src/misc/test_utils.rs +++ b/editor/src/misc/test_utils.rs @@ -8,6 +8,8 @@ use graphene::color::Color; /// A set of utility functions to make the writing of editor test more declarative pub trait EditorTestUtils { + fn new_document(&mut self); + fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); fn draw_shape(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64); @@ -24,6 +26,10 @@ pub trait EditorTestUtils { } impl EditorTestUtils for Editor { + fn new_document(&mut self) { + self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") })); + } + fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) { self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2); } diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index f2e39688f6..50939215a8 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -40,6 +40,7 @@ pub struct SignalToMessageMap { pub trait ToolTransition { fn signal_to_message_map(&self) -> SignalToMessageMap; + fn activate(&self, responses: &mut VecDeque) { let mut subscribe_message = |broadcast_to_tool_mapping: Option, signal: BroadcastSignal| { if let Some(mapping) = broadcast_to_tool_mapping { @@ -78,6 +79,7 @@ pub trait ToolTransition { unsubscribe_message(signal_to_tool_map.selection_changed, BroadcastSignal::SelectionChanged); } } + pub trait ToolMetadata { fn icon_name(&self) -> String; fn tooltip(&self) -> String; diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index 570d2a4c9e..93a51300dd 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -75,10 +75,10 @@ pub enum ToolMessage { // Detail(DetailToolMessage), // Messages - #[remain::unsorted] ActivateTool { tool_type: ToolType, }, + DeactivateTools, InitTools, ResetColors, SelectPrimaryColor { diff --git a/editor/src/viewport_tools/tool_message_handler.rs b/editor/src/viewport_tools/tool_message_handler.rs index d23f281b0f..f536919e16 100644 --- a/editor/src/viewport_tools/tool_message_handler.rs +++ b/editor/src/viewport_tools/tool_message_handler.rs @@ -1,9 +1,11 @@ -use super::tool::{message_to_tool_type, DocumentToolData, ToolFsmState}; +use super::tool::{message_to_tool_type, ToolFsmState}; use crate::document::DocumentMessageHandler; use crate::input::InputPreprocessorMessageHandler; use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::PropertyHolder; +use crate::layout::widgets::{IconButton, Layout, LayoutGroup, SwatchPairInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; +use crate::viewport_tools::tool::DocumentToolData; use graphene::color::Color; use graphene::layers::text_layer::FontCache; @@ -73,12 +75,16 @@ impl MessageHandler { + let tool_data = &mut self.tool_state.tool_data; + tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses); + } InitTools => { let tool_data = &mut self.tool_state.tool_data; let document_data = &self.tool_state.document_tool_data; let active_tool = &tool_data.active_tool_type; - // subscribe tool to broadcast messages + // Subscribe tool to broadcast messages tool_data.tools.get(active_tool).unwrap().activate(responses); // Register initial properties @@ -87,6 +93,10 @@ impl MessageHandler ActionList { - let mut list = actions!(ToolMessageDiscriminant; SelectRandomPrimaryColor, ResetColors, SwapColors, ActivateTool); + let mut list = actions!(ToolMessageDiscriminant; + ActivateTool, + SelectRandomPrimaryColor, + ResetColors, + SwapColors, + ); list.extend(self.tool_state.tool_data.active_tool().actions()); list @@ -164,10 +179,37 @@ impl MessageHandler) { + let layout = WidgetLayout::new(vec![ + LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::SwatchPairInput(SwatchPairInput { + primary: document_data.primary_color, + secondary: document_data.secondary_color, + }))], + }, + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::IconButton(IconButton { + size: 16, + icon: "Swap".into(), + tooltip: "Swap (Shift+X)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut + on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()), + ..Default::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 16, + icon: "ResetColors".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut + tooltip: "Reset (Ctrl+Shift+X)".into(), + on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()), + ..Default::default() + })), + ], + }, + ]); + responses.push_back( - FrontendMessage::UpdateWorkingColors { - primary: document_data.primary_color, - secondary: document_data.secondary_color, + LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(layout), + layout_target: LayoutTarget::WorkingColors, } .into(), ); diff --git a/editor/src/viewport_tools/tools/artboard_tool.rs b/editor/src/viewport_tools/tools/artboard_tool.rs index 71fc038da6..6e039c1d3b 100644 --- a/editor/src/viewport_tools/tools/artboard_tool.rs +++ b/editor/src/viewport_tools/tools/artboard_tool.rs @@ -73,7 +73,13 @@ impl<'a> MessageHandler> for ArtboardTool } } - advertise_actions!(ArtboardToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, DeleteSelected, Abort); + advertise_actions!(ArtboardToolMessageDiscriminant; + PointerDown, + PointerUp, + PointerMove, + DeleteSelected, + Abort, + ); } impl PropertyHolder for ArtboardTool {} diff --git a/editor/src/viewport_tools/tools/ellipse_tool.rs b/editor/src/viewport_tools/tools/ellipse_tool.rs index eb4db7b311..1118f6b402 100644 --- a/editor/src/viewport_tools/tools/ellipse_tool.rs +++ b/editor/src/viewport_tools/tools/ellipse_tool.rs @@ -75,8 +75,14 @@ impl<'a> MessageHandler> for EllipseTool use EllipseToolFsmState::*; match self.fsm_state { - Ready => actions!(EllipseToolMessageDiscriminant; DragStart), - Drawing => actions!(EllipseToolMessageDiscriminant; DragStop, Abort, Resize), + Ready => actions!(EllipseToolMessageDiscriminant; + DragStart, + ), + Drawing => actions!(EllipseToolMessageDiscriminant; + DragStop, + Abort, + Resize, + ), } } } diff --git a/editor/src/viewport_tools/tools/eyedropper_tool.rs b/editor/src/viewport_tools/tools/eyedropper_tool.rs index b903055b87..1c62f5dcb4 100644 --- a/editor/src/viewport_tools/tools/eyedropper_tool.rs +++ b/editor/src/viewport_tools/tools/eyedropper_tool.rs @@ -66,7 +66,10 @@ impl<'a> MessageHandler> for EyedropperTo } } - advertise_actions!(EyedropperToolMessageDiscriminant; LeftMouseDown, RightMouseDown); + advertise_actions!(EyedropperToolMessageDiscriminant; + LeftMouseDown, + RightMouseDown, + ); } impl ToolTransition for EyedropperTool { diff --git a/editor/src/viewport_tools/tools/fill_tool.rs b/editor/src/viewport_tools/tools/fill_tool.rs index 100cae7fc8..9394b935fa 100644 --- a/editor/src/viewport_tools/tools/fill_tool.rs +++ b/editor/src/viewport_tools/tools/fill_tool.rs @@ -67,7 +67,10 @@ impl<'a> MessageHandler> for FillTool { } } - advertise_actions!(FillToolMessageDiscriminant; LeftMouseDown, RightMouseDown); + advertise_actions!(FillToolMessageDiscriminant; + LeftMouseDown, + RightMouseDown, + ); } impl ToolTransition for FillTool { diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 17578c0de5..5cc4605752 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -115,8 +115,16 @@ impl<'a> MessageHandler> for FreehandTool use FreehandToolFsmState::*; match self.fsm_state { - Ready => actions!(FreehandToolMessageDiscriminant; DragStart, DragStop, Abort), - Drawing => actions!(FreehandToolMessageDiscriminant; DragStop, PointerMove, Abort), + Ready => actions!(FreehandToolMessageDiscriminant; + DragStart, + DragStop, + Abort, + ), + Drawing => actions!(FreehandToolMessageDiscriminant; + DragStop, + PointerMove, + Abort, + ), } } } diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs index 9521602f84..a0a380bb59 100644 --- a/editor/src/viewport_tools/tools/gradient_tool.rs +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -98,7 +98,12 @@ impl<'a> MessageHandler> for GradientTool } } - advertise_actions!(GradientToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, Abort); + advertise_actions!(GradientToolMessageDiscriminant; + PointerDown, + PointerUp, + PointerMove, + Abort, + ); } impl PropertyHolder for GradientTool { diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 901cf841f9..a12c103ce0 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -116,8 +116,14 @@ impl<'a> MessageHandler> for LineTool { use LineToolFsmState::*; match self.fsm_state { - Ready => actions!(LineToolMessageDiscriminant; DragStart), - Drawing => actions!(LineToolMessageDiscriminant; DragStop, Redraw, Abort), + Ready => actions!(LineToolMessageDiscriminant; + DragStart, + ), + Drawing => actions!(LineToolMessageDiscriminant; + DragStop, + Redraw, + Abort, + ), } } } diff --git a/editor/src/viewport_tools/tools/navigate_tool.rs b/editor/src/viewport_tools/tools/navigate_tool.rs index 20d970de95..f4ec3ba8d2 100644 --- a/editor/src/viewport_tools/tools/navigate_tool.rs +++ b/editor/src/viewport_tools/tools/navigate_tool.rs @@ -75,8 +75,16 @@ impl<'a> MessageHandler> for NavigateTool use NavigateToolFsmState::*; match self.fsm_state { - Ready => actions!(NavigateToolMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin), - _ => actions!(NavigateToolMessageDiscriminant; ClickZoom, PointerMove, TransformCanvasEnd), + Ready => actions!(NavigateToolMessageDiscriminant; + TranslateCanvasBegin, + RotateCanvasBegin, + ZoomCanvasBegin, + ), + _ => actions!(NavigateToolMessageDiscriminant; + ClickZoom, + PointerMove, + TransformCanvasEnd, + ), } } } diff --git a/editor/src/viewport_tools/tools/path_tool.rs b/editor/src/viewport_tools/tools/path_tool.rs index cc3259dae4..6c91285145 100644 --- a/editor/src/viewport_tools/tools/path_tool.rs +++ b/editor/src/viewport_tools/tools/path_tool.rs @@ -85,8 +85,15 @@ impl<'a> MessageHandler> for PathTool { use PathToolFsmState::*; match self.fsm_state { - Ready => actions!(PathToolMessageDiscriminant; DragStart, Delete), - Dragging => actions!(PathToolMessageDiscriminant; DragStop, PointerMove, Delete), + Ready => actions!(PathToolMessageDiscriminant; + DragStart, + Delete, + ), + Dragging => actions!(PathToolMessageDiscriminant; + DragStop, + PointerMove, + Delete, + ), } } } diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index e833144e43..4539de2735 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -131,8 +131,20 @@ impl<'a> MessageHandler> for PenTool { fn actions(&self) -> ActionList { match self.fsm_state { - PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort), - PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort), + PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant; + Undo, + DragStart, + DragStop, + Confirm, + Abort, + ), + PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; + DragStart, + DragStop, + PointerMove, + Confirm, + Abort, + ), } } } diff --git a/editor/src/viewport_tools/tools/rectangle_tool.rs b/editor/src/viewport_tools/tools/rectangle_tool.rs index 4de0cbbb06..55c76a8d58 100644 --- a/editor/src/viewport_tools/tools/rectangle_tool.rs +++ b/editor/src/viewport_tools/tools/rectangle_tool.rs @@ -63,8 +63,14 @@ impl<'a> MessageHandler> for RectangleToo use RectangleToolFsmState::*; match self.fsm_state { - Ready => actions!(RectangleToolMessageDiscriminant; DragStart), - Drawing => actions!(RectangleToolMessageDiscriminant; DragStop, Abort, Resize), + Ready => actions!(RectangleToolMessageDiscriminant; + DragStart, + ), + Drawing => actions!(RectangleToolMessageDiscriminant; + DragStop, + Abort, + Resize, + ), } } } diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index 617749c704..c3ac6f42f5 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -161,8 +161,9 @@ impl PropertyHolder for SelectTool { separator_type: SeparatorType::Related, })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Align".into(), + header: "Align".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { direction: SeparatorDirection::Horizontal, @@ -187,8 +188,9 @@ impl PropertyHolder for SelectTool { separator_type: SeparatorType::Related, })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Flip".into(), + header: "Flip".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { direction: SeparatorDirection::Horizontal, @@ -234,8 +236,9 @@ impl PropertyHolder for SelectTool { separator_type: SeparatorType::Related, })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { - title: "Boolean".into(), + header: "Boolean".into(), text: "The contents of this popover menu are coming soon".into(), + ..Default::default() })), ], }])) @@ -266,8 +269,18 @@ impl<'a> MessageHandler> for SelectTool { use SelectToolFsmState::*; match self.fsm_state { - Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer), - _ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer), + Ready => actions!(SelectToolMessageDiscriminant; + DragStart, + PointerMove, + Abort, + EditLayer, + ), + _ => actions!(SelectToolMessageDiscriminant; + DragStop, + PointerMove, + Abort, + EditLayer, + ), } } } diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 0ac1f4d6d7..2c56e51975 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -114,8 +114,14 @@ impl<'a> MessageHandler> for ShapeTool { use ShapeToolFsmState::*; match self.fsm_state { - Ready => actions!(ShapeToolMessageDiscriminant; DragStart), - Drawing => actions!(ShapeToolMessageDiscriminant; DragStop, Abort, Resize), + Ready => actions!(ShapeToolMessageDiscriminant; + DragStart, + ), + Drawing => actions!(ShapeToolMessageDiscriminant; + DragStop, + Abort, + Resize, + ), } } } diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index aa5f929e13..b75a2f3bc4 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -119,8 +119,19 @@ impl<'a> MessageHandler> for SplineTool { use SplineToolFsmState::*; match self.fsm_state { - Ready => actions!(SplineToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort), - Drawing => actions!(SplineToolMessageDiscriminant; DragStop, PointerMove, Confirm, Abort), + Ready => actions!(SplineToolMessageDiscriminant; + Undo, + DragStart, + DragStop, + Confirm, + Abort, + ), + Drawing => actions!(SplineToolMessageDiscriminant; + DragStop, + PointerMove, + Confirm, + Abort, + ), } } } diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index e4773b5919..14787334a9 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -96,6 +96,7 @@ impl PropertyHolder for TextTool { }) .into() }), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { direction: SeparatorDirection::Horizontal, @@ -112,6 +113,7 @@ impl PropertyHolder for TextTool { }) .into() }), + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { direction: SeparatorDirection::Horizontal, @@ -169,8 +171,14 @@ impl<'a> MessageHandler> for TextTool { use TextToolFsmState::*; match self.fsm_state { - Ready => actions!(TextMessageDiscriminant; Interact), - Editing => actions!(TextMessageDiscriminant; Interact, Abort, CommitText), + Ready => actions!(TextMessageDiscriminant; + Interact, + ), + Editing => actions!(TextMessageDiscriminant; + Interact, + Abort, + CommitText, + ), } } } diff --git a/frontend/assets/graphics/graphite-logotype-solid.svg b/frontend/assets/graphics/graphite-logotype-solid.svg new file mode 100644 index 0000000000..466bb496c8 --- /dev/null +++ b/frontend/assets/graphics/graphite-logotype-solid.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/assets/icon-16px-solid/folder.svg b/frontend/assets/icon-16px-solid/folder.svg new file mode 100644 index 0000000000..0f9cb44015 --- /dev/null +++ b/frontend/assets/icon-16px-solid/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 7423478ff3..957bcd9598 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -213,6 +213,7 @@ img { diff --git a/frontend/src/components/widgets/buttons/IconButton.vue b/frontend/src/components/widgets/buttons/IconButton.vue index 765529857e..facfd3834f 100644 --- a/frontend/src/components/widgets/buttons/IconButton.vue +++ b/frontend/src/components/widgets/buttons/IconButton.vue @@ -1,5 +1,5 @@ @@ -69,11 +69,13 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue"; export default defineComponent({ props: { - action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true }, icon: { type: String as PropType, required: true }, size: { type: Number as PropType, required: true }, active: { type: Boolean as PropType, default: false }, - gapAfter: { type: Boolean as PropType, default: false }, + tooltip: { type: String as PropType, required: false }, + + // Callbacks + action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true }, }, components: { IconLabel }, }); diff --git a/frontend/src/components/widgets/buttons/PopoverButton.vue b/frontend/src/components/widgets/buttons/PopoverButton.vue index 57fd146d94..9573c17f8c 100644 --- a/frontend/src/components/widgets/buttons/PopoverButton.vue +++ b/frontend/src/components/widgets/buttons/PopoverButton.vue @@ -62,8 +62,10 @@ export default defineComponent({ LayoutRow, }, props: { - action: { type: Function as PropType<() => void>, required: false }, icon: { type: String as PropType, default: "DropdownArrow" }, + + // Callbacks + action: { type: Function as PropType<() => void>, required: false }, }, data() { return { diff --git a/frontend/src/components/widgets/buttons/TextButton.ts b/frontend/src/components/widgets/buttons/TextButton.ts index ece61a3f52..a7254059ed 100644 --- a/frontend/src/components/widgets/buttons/TextButton.ts +++ b/frontend/src/components/widgets/buttons/TextButton.ts @@ -1,16 +1,17 @@ // TODO: Try and get rid of the need for this file export interface TextButtonWidget { - kind: "TextButton"; tooltip?: string; message?: string | object; callback?: () => void; props: { - // `action` is used via `IconButtonWidget.callback` + kind: "TextButton"; label: string; emphasized?: boolean; - disabled?: boolean; minWidth?: number; - gapAfter?: boolean; + disabled?: boolean; + + // Callbacks + // `action` is used via `IconButtonWidget.callback` }; } diff --git a/frontend/src/components/widgets/buttons/TextButton.vue b/frontend/src/components/widgets/buttons/TextButton.vue index a3e2493024..3acd251f60 100644 --- a/frontend/src/components/widgets/buttons/TextButton.vue +++ b/frontend/src/components/widgets/buttons/TextButton.vue @@ -62,12 +62,13 @@ import TextLabel from "@/components/widgets/labels/TextLabel.vue"; export default defineComponent({ props: { - action: { type: Function as PropType<(e: MouseEvent) => void>, required: true }, label: { type: String as PropType, required: true }, emphasized: { type: Boolean as PropType, default: false }, - disabled: { type: Boolean as PropType, default: false }, minWidth: { type: Number as PropType, default: 0 }, - gapAfter: { type: Boolean as PropType, default: false }, + disabled: { type: Boolean as PropType, default: false }, + + // Callbacks + action: { type: Function as PropType<(e: MouseEvent) => void>, required: true }, }, components: { TextLabel }, }); diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.vue b/frontend/src/components/widgets/inputs/CheckboxInput.vue index cb341486a1..f3360e689c 100644 --- a/frontend/src/components/widgets/inputs/CheckboxInput.vue +++ b/frontend/src/components/widgets/inputs/CheckboxInput.vue @@ -1,7 +1,7 @@