diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f48a1c56a..59b061584f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,5 +52,6 @@ "files.insertFinalNewline": true, "files.associations": { "*.graphite": "json" - } + }, + "rust-analyzer.checkOnSave": false } diff --git a/editor/src/messages/dialog/dialog_message.rs b/editor/src/messages/dialog/dialog_message.rs index baee1e6582..4ae8d546db 100644 --- a/editor/src/messages/dialog/dialog_message.rs +++ b/editor/src/messages/dialog/dialog_message.rs @@ -8,8 +8,6 @@ pub enum DialogMessage { ExportDialog(ExportDialogMessage), #[child] NewDocumentDialog(NewDocumentDialogMessage), - #[child] - PreferencesDialog(PreferencesDialogMessage), // Messages CloseAllDocumentsWithConfirmation, diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 03e1f12e92..aaf68f1df1 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -1,5 +1,5 @@ use super::new_document_dialog::NewDocumentDialogMessageContext; -use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog}; +use super::simple_dialogs::{self, *}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; @@ -16,7 +16,6 @@ pub struct DialogMessageContext<'a> { pub struct DialogMessageHandler { export_dialog: ExportDialogMessageHandler, new_document_dialog: NewDocumentDialogMessageHandler, - preferences_dialog: PreferencesDialogMessageHandler, } #[message_handler_data] @@ -31,7 +30,6 @@ impl MessageHandler> for DialogMessageHa match message { DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ExportDialogMessageContext { portfolio }), DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, NewDocumentDialogMessageContext { viewport_bounds }), - DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }), DialogMessage::CloseAllDocumentsWithConfirmation => { let dialog = simple_dialogs::CloseAllDocumentsDialog { @@ -40,13 +38,13 @@ impl MessageHandler> for DialogMessageHa dialog.send_dialog_to_frontend(responses); } DialogMessage::CloseDialogAndThen { followups } => { + // Since this message is "close dialog and then", the closing of the dialogue must happen first. + // This is because processing may spawn another dialogue (e.g. the export dialogue may produce an error dialogue). + responses.add(FrontendMessage::DisplayDialogDismiss); + for message in followups.into_iter() { responses.add(message); } - - // This come after followups, so that the followups (which can cause the dialog to open) happen first, then we close it afterwards. - // If it comes before, the dialog reopens (and appears to not close at all). - responses.add(FrontendMessage::DisplayDialogDismiss); } DialogMessage::DisplayDialogError { title, description } => { let dialog = simple_dialogs::ErrorDialog { title, description }; @@ -112,8 +110,8 @@ impl MessageHandler> for DialogMessageHa self.new_document_dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestPreferencesDialog => { - self.preferences_dialog = PreferencesDialogMessageHandler {}; - self.preferences_dialog.send_dialog_to_frontend(responses, preferences); + let dialog = PreferencesDialog { preferences }; + dialog.send_dialog_to_frontend(responses); } } } diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 0fa126917b..9ac9a21f96 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -43,16 +43,22 @@ impl MessageHandler> for Exp ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background, ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area, - ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport { - file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), - file_type: self.file_type, - scale_factor: self.scale_factor, - bounds: self.bounds, - transparent_background: self.file_type != FileType::Jpg && self.transparent_background, - }), + ExportDialogMessage::Submit => { + responses.add(FrontendMessage::DisplayDialogDismiss); + responses.add(PortfolioMessage::SubmitDocumentExport { + file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), + file_type: self.file_type, + scale_factor: self.scale_factor, + bounds: self.bounds, + transparent_background: self.file_type != FileType::Jpg && self.transparent_background, + }); + } } - self.send_dialog_to_frontend(responses); + // Don't send the dialogue if the form was already submitted + if message != ExportDialogMessage::Submit { + self.send_dialog_to_frontend(responses); + } } advertise_actions! {ExportDialogUpdate;} @@ -64,15 +70,7 @@ impl DialogLayoutHolder for ExportDialogMessageHandler { fn layout_buttons(&self) -> Layout { let widgets = vec![ - TextButton::new("Export") - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![ExportDialogMessage::Submit.into()], - } - .into() - }) - .widget_holder(), + TextButton::new("Export").emphasized(true).on_update(|_| ExportDialogMessage::Submit.into()).widget_holder(), TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; diff --git a/editor/src/messages/dialog/mod.rs b/editor/src/messages/dialog/mod.rs index 67a186f993..dd2f1ba716 100644 --- a/editor/src/messages/dialog/mod.rs +++ b/editor/src/messages/dialog/mod.rs @@ -10,7 +10,6 @@ mod dialog_message_handler; pub mod export_dialog; pub mod new_document_dialog; -pub mod preferences_dialog; pub mod simple_dialogs; #[doc(inline)] diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 51cf615184..4e0e76f738 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -19,12 +19,16 @@ pub struct NewDocumentDialogMessageHandler { #[message_handler_data] impl<'a> MessageHandler> for NewDocumentDialogMessageHandler { fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque, context: NewDocumentDialogMessageContext<'a>) { + let mut dismiss = false; + match message { NewDocumentDialogMessage::Name(name) => self.name = name, NewDocumentDialogMessage::Infinite(infinite) => self.infinite = infinite, NewDocumentDialogMessage::DimensionsX(x) => self.dimensions.x = x as u32, NewDocumentDialogMessage::DimensionsY(y) => self.dimensions.y = y as u32, NewDocumentDialogMessage::Submit => { + responses.add(FrontendMessage::DisplayDialogDismiss); + dismiss = true; responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }); let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0; @@ -48,7 +52,10 @@ impl<'a> MessageHandler Layout { let widgets = vec![ - TextButton::new("OK") - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![NewDocumentDialogMessage::Submit.into()], - } - .into() - }) - .widget_holder(), + TextButton::new("OK").emphasized(true).on_update(|_| NewDocumentDialogMessage::Submit.into()).widget_holder(), TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; diff --git a/editor/src/messages/dialog/preferences_dialog/mod.rs b/editor/src/messages/dialog/preferences_dialog/mod.rs deleted file mode 100644 index eb5ce03843..0000000000 --- a/editor/src/messages/dialog/preferences_dialog/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod preferences_dialog_message; -mod preferences_dialog_message_handler; - -#[doc(inline)] -pub use preferences_dialog_message::{PreferencesDialogMessage, PreferencesDialogMessageDiscriminant}; -#[doc(inline)] -pub use preferences_dialog_message_handler::{PreferencesDialogMessageContext, PreferencesDialogMessageHandler}; diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs deleted file mode 100644 index 736fe7c10a..0000000000 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::messages::prelude::*; - -#[impl_message(Message, DialogMessage, PreferencesDialog)] -#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum PreferencesDialogMessage { - Confirm, -} diff --git a/editor/src/messages/dialog/simple_dialogs/mod.rs b/editor/src/messages/dialog/simple_dialogs/mod.rs index a330efac20..6f5148fda4 100644 --- a/editor/src/messages/dialog/simple_dialogs/mod.rs +++ b/editor/src/messages/dialog/simple_dialogs/mod.rs @@ -5,6 +5,7 @@ mod coming_soon_dialog; mod demo_artwork_dialog; mod error_dialog; mod licenses_dialog; +mod preferences_dialog; pub use about_graphite_dialog::AboutGraphiteDialog; pub use close_all_documents_dialog::CloseAllDocumentsDialog; @@ -14,3 +15,4 @@ pub use demo_artwork_dialog::ARTWORK; pub use demo_artwork_dialog::DemoArtworkDialog; pub use error_dialog::ErrorDialog; pub use licenses_dialog::LicensesDialog; +pub use preferences_dialog::PreferencesDialog; diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/simple_dialogs/preferences_dialog.rs similarity index 77% rename from editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs rename to editor/src/messages/dialog/simple_dialogs/preferences_dialog.rs index 26bbd4c2b8..d0223b497b 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/simple_dialogs/preferences_dialog.rs @@ -4,38 +4,28 @@ use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; -#[derive(ExtractField)] -pub struct PreferencesDialogMessageContext<'a> { +pub struct PreferencesDialog<'a> { pub preferences: &'a PreferencesMessageHandler, } -/// A dialog to allow users to customize Graphite editor options -#[derive(Debug, Clone, Default, ExtractField)] -pub struct PreferencesDialogMessageHandler {} - -#[message_handler_data] -impl MessageHandler> for PreferencesDialogMessageHandler { - fn process_message(&mut self, message: PreferencesDialogMessage, responses: &mut VecDeque, context: PreferencesDialogMessageContext) { - let PreferencesDialogMessageContext { preferences } = context; +impl<'a> DialogLayoutHolder for PreferencesDialog<'a> { + const ICON: &'static str = "Settings"; + const TITLE: &'static str = "Editor Preferences"; - match message { - PreferencesDialogMessage::Confirm => {} - } + fn layout_buttons(&self) -> Layout { + let widgets = vec![ + TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), + TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(), + ]; - self.send_dialog_to_frontend(responses, preferences); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } - - advertise_actions! {PreferencesDialogUpdate;} } -// This doesn't actually implement the `DialogLayoutHolder` trait like the other dialog message handlers. -// That's because we need to give `send_layout` the `preferences` argument, which is not part of the trait. -// However, it's important to keep the methods in sync with those from the trait for consistency. -impl PreferencesDialogMessageHandler { - const ICON: &'static str = "Settings"; - const TITLE: &'static str = "Editor Preferences"; +impl<'a> LayoutHolder for PreferencesDialog<'a> { + fn layout(&self) -> Layout { + let preferences = self.preferences; - fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { // ========== // NAVIGATION // ========== @@ -217,58 +207,6 @@ impl PreferencesDialogMessageHandler { LayoutGroup::Row { widgets: vector_meshes }, ])) } - - pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { - responses.add(LayoutMessage::SendLayout { - layout: self.layout(preferences), - layout_target, - }) - } - - fn layout_column_2(&self) -> Layout { - Layout::default() - } - - fn send_layout_column_2(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { - responses.add(LayoutMessage::SendLayout { - layout: self.layout_column_2(), - layout_target, - }); - } - - fn layout_buttons(&self) -> Layout { - let widgets = vec![ - TextButton::new("OK") - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![PreferencesDialogMessage::Confirm.into()], - } - .into() - }) - .widget_holder(), - TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(), - ]; - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } - - fn send_layout_buttons(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { - responses.add(LayoutMessage::SendLayout { - layout: self.layout_buttons(), - layout_target, - }); - } - - pub fn send_dialog_to_frontend(&self, responses: &mut VecDeque, preferences: &PreferencesMessageHandler) { - self.send_layout(responses, LayoutTarget::DialogColumn1, preferences); - self.send_layout_column_2(responses, LayoutTarget::DialogColumn2); - self.send_layout_buttons(responses, LayoutTarget::DialogButtons); - responses.add(FrontendMessage::DisplayDialog { - icon: Self::ICON.into(), - title: Self::TITLE.into(), - }); - } } /// Maps display values (1-100) to actual zoom rates. diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 1b722301e6..906547021f 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -165,7 +165,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseLeft); action_dispatch=GradientToolMessage::PointerDown), entry!(PointerMove; refresh_keys=[Shift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: Shift }), entry!(KeyUp(MouseLeft); action_dispatch=GradientToolMessage::PointerUp), - entry!(DoubleClick(MouseButton::Left); action_dispatch=GradientToolMessage::InsertStop), + entry!(DoubleClick(MouseButton::Left); action_dispatch=GradientToolMessage::InsertStopProxy), entry!(KeyDown(Delete); action_dispatch=GradientToolMessage::DeleteStop), entry!(KeyDown(Backspace); action_dispatch=GradientToolMessage::DeleteStop), entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort), diff --git a/editor/src/messages/input_mapper/utility_types/input_mouse.rs b/editor/src/messages/input_mapper/utility_types/input_mouse.rs index a9b746608d..7644972d99 100644 --- a/editor/src/messages/input_mapper/utility_types/input_mouse.rs +++ b/editor/src/messages/input_mapper/utility_types/input_mouse.rs @@ -78,7 +78,8 @@ impl ScrollDelta { } } -// TODO: Document the difference between this and EditorMouseState +/// This is similar to [`EditorMouseState`] except that the position is stored relative to the viewport rather than relative to the UI. +/// Convert using [`EditorMouseState::to_mouse_state`] #[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct MouseState { pub position: ViewportPosition, @@ -94,7 +95,8 @@ impl MouseState { } } -// TODO: Document the difference between this and MouseState +/// This is similar to [`MouseState`] except that the position is stored relative to the UI rather than relative to the viewport. +/// Convert using [`EditorMouseState::to_mouse_state`] #[derive(Debug, Copy, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EditorMouseState { pub editor_position: EditorPosition, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5bfa0af7dd..e404aea8d1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -662,7 +662,7 @@ impl MessageHandler> for DocumentMes return; } - let layers_to_move = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path); + let layers_to_move = self.network_interface.shallowest_unique_layers(&self.selection_network_path).collect::>(); // Offset the index for layers to move that are below another layer to move. For example when moving 1 and 2 between 3 and 4, 2 should be inserted at the same index as 1 since 1 is moved first. let layers_to_move_with_insert_offset = layers_to_move .iter() @@ -717,7 +717,7 @@ impl MessageHandler> for DocumentMes } DocumentMessage::MoveSelectedLayersToGroup { parent } => { // Group all shallowest unique selected layers in order - let all_layers_to_group_sorted = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path); + let all_layers_to_group_sorted = self.network_interface.shallowest_unique_layers(&self.selection_network_path).collect::>(); for layer_to_group in all_layers_to_group_sorted.into_iter().rev() { responses.add(NodeGraphMessage::MoveLayerToStack { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 59c5b2657f..32b33fbcb6 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -200,6 +200,7 @@ impl core::hash::Hash for OverlayContext { } impl OverlayContext { + #[cfg(not(test))] pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self { Self { internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))), @@ -421,6 +422,7 @@ impl Default for OverlayContextInternal { } impl OverlayContextInternal { + #[cfg(not(test))] pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self { Self { scene: Scene::new(), diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index cc7073e8a2..b907a42a19 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -451,6 +451,19 @@ impl LayerNodeIdentifier { } } +impl PartialEq for LayerNodeIdentifier { + fn eq(&self, other: &NodeId) -> bool { + self.to_node() == *other + } +} + +// Implement == comparisons +impl PartialEq for NodeId { + fn eq(&self, other: &LayerNodeIdentifier) -> bool { + other.to_node() == *self + } +} + // ======== // AxisIter // ======== diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 938bd24d5a..a4aebec174 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1314,46 +1314,14 @@ impl NodeNetworkInterface { .reduce(Quad::combine_bounds) } - /// Layers excluding ones that are children of other layers in the list. + /// Layers excluding ones that are children of other layers in the list in layer tree order. // TODO: Cache this - pub fn shallowest_unique_layers(&self, network_path: &[NodeId]) -> impl Iterator + use<> { - let mut sorted_layers = if let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) { - selected_nodes - .selected_layers(self.document_metadata()) - .map(|layer| { - let mut layer_path = layer.ancestors(&self.document_metadata).collect::>(); - layer_path.reverse(); - layer_path - }) - .collect::>() - } else { - log::error!("Could not get selected nodes in shallowest_unique_layers"); - Vec::new() - }; - - // Sorting here creates groups of similar UUID paths - sorted_layers.sort(); - sorted_layers.dedup_by(|a, b| a.starts_with(b)); - sorted_layers.into_iter().map(|mut path| { - let layer = path.pop().expect("Path should not be empty"); - assert!( - layer != LayerNodeIdentifier::ROOT_PARENT, - "The root parent cannot be selected, so it cannot be a shallowest selected layer" - ); - layer - }) - } - - pub fn shallowest_unique_layers_sorted(&self, network_path: &[NodeId]) -> Vec { - let all_layers_to_group = self.shallowest_unique_layers(network_path).collect::>(); - // Ensure nodes are grouped in the correct order - let mut all_layers_to_group_sorted = Vec::new(); - for descendant in LayerNodeIdentifier::ROOT_PARENT.descendants(self.document_metadata()) { - if all_layers_to_group.contains(&descendant) { - all_layers_to_group_sorted.push(descendant); - }; - } - all_layers_to_group_sorted + // Now allocation free! + pub fn shallowest_unique_layers(&self, network_path: &[NodeId]) -> ShallowestSelectionIter<'_, NodeId> { + // Avoids the clone and filtering from from the selected_nodes_in_nested_network. + let metadata = self.network_metadata(network_path); + let selection = metadata.and_then(|metadata| metadata.persistent_metadata.selection_undo_history.back()); + ShallowestSelectionIter::new(self.document_metadata(), selection.map_or([].as_slice(), |selection| selection.0.as_slice())) } /// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root. Skips selected non-folder, non-artboard layers @@ -7022,3 +6990,42 @@ pub enum TransactionStatus { #[default] Finished, } + +/// Iterate through the shallowest selected layers without allocating +#[derive(Clone)] +pub struct ShallowestSelectionIter<'a, T: PartialEq> { + next: Option, + selection: &'a [T], // TODO: should be HashSet to avoid duplicates. + metadata: &'a DocumentMetadata, +} + +impl<'a, T: PartialEq> ShallowestSelectionIter<'a, T> { + pub fn new(metadata: &'a DocumentMetadata, selection: &'a [T]) -> Self { + ShallowestSelectionIter { + selection, + next: Some(LayerNodeIdentifier::ROOT_PARENT), + metadata, + } + } +} + +impl> Iterator for ShallowestSelectionIter<'_, T> { + type Item = LayerNodeIdentifier; + + fn next(&mut self) -> Option { + while let Some(layer_node) = self.next.take() { + // Ignoring the children of this layer, find the next layer that would be displayed in the tree + let below_in_tree = || layer_node.ancestors(self.metadata).find_map(|ancestor| ancestor.next_sibling(self.metadata)); + + // If the current layer is selected, return it. + if layer_node != LayerNodeIdentifier::ROOT_PARENT && self.selection.iter().any(|selection| *selection == layer_node) { + self.next = below_in_tree(); // Go straight to below and don't look at children + return Some(layer_node); + } + // Go to children or otherwise go to below in the tree + self.next = layer_node.first_child(self.metadata).or_else(below_in_tree); + } + + None + } +} diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54ed..720e88e0c4 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -15,7 +15,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector; -use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document_migration::*; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; @@ -259,12 +258,7 @@ impl MessageHandler> for Portfolio }; let mut copy_val = |buffer: &mut Vec| { - let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::>(); - - ordered_last_elements.sort_by_key(|layer| { - let Some(parent) = layer.parent(active_document.metadata()) else { return usize::MAX }; - DocumentMessageHandler::get_calculated_insert_index(active_document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent) - }); + let ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::>(); for layer in ordered_last_elements.into_iter() { let layer_node_id = layer.to_node(); diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 1f12aad6b1..403c87c9ec 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -9,7 +9,6 @@ pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMe pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler}; pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler}; pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler}; -pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler}; pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler}; diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index 1950b7a5bc..cfb522fd52 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -5,7 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::path_tool::PathOptionsUpdate; -use crate::messages::tool::tool_messages::select_tool::SelectOptionsUpdate; +use crate::messages::tool::tool_messages::select_tool::options::SelectOptionsUpdate; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::{DAffine2, DVec2}; use graphene_std::transform::ReferencePoint; diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 45a1740f91..1808644c47 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1596,7 +1596,7 @@ impl ShapeState { } pub fn find_nearest_visible_point_indices( - &mut self, + &self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, diff --git a/editor/src/messages/tool/common_functionality/shapes/line_shape.rs b/editor/src/messages/tool/common_functionality/shapes/line_shape.rs index 8bd22b0b16..f53ee84eb7 100644 --- a/editor/src/messages/tool/common_functionality/shapes/line_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/line_shape.rs @@ -70,13 +70,16 @@ impl Line { return; }; + let transform_from_document = document.metadata().transform_to_document(layer).inverse(); + let layer_points = document_points.map(|point| transform_from_document.transform_point2(point)); + responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false), + input: NodeInput::value(TaggedValue::DVec2(layer_points[0]), false), }); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false), + input: NodeInput::value(TaggedValue::DVec2(layer_points[1]), false), }); responses.add(NodeGraphMessage::RunDocumentGraph); } @@ -351,33 +354,27 @@ mod test_line_tool { let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard"); + let transform = DAffine2::from_angle(45_f64.to_radians()); editor .handle_message(GraphOperationMessage::TransformChange { layer: artboard_id, - transform: DAffine2::from_angle(45_f64.to_radians()), + transform, transform_in: TransformIn::Local, skip_rerender: false, }) .await; - editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await; + let expected_start = DVec2::new(55., 42.); + let expected_end = DVec2::new(124., 142.); + editor + .drag_tool(ToolType::Line, expected_start.x, expected_start.y, expected_end.x, expected_end.y, ModifierKeys::empty()) + .await; let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard"); - // The line should still be diagonal with equal change in x and y - let line_vector = end_input - start_input; - // Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square) - let line_length = line_vector.length(); - assert!( - (line_length - 141.42).abs() < 1., // 100 * sqrt(2) ~= 141.42 - "Line length should be approximately 141.42 units. Got: {line_length}" - ); - assert!((line_vector.x - 100.).abs() < 1., "X-component of line vector should be approximately 100. Got: {}", line_vector.x); - assert!( - (line_vector.y.abs() - 100.).abs() < 1., - "Absolute Y-component of line vector should be approximately 100. Got: {}", - line_vector.y.abs() - ); - let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees(); - assert!((angle_degrees - (-45.)).abs() < 1., "Line angle should be close to -45 degrees. Got: {angle_degrees}"); + let document = editor.editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap(); + assert_eq!(document.metadata().document_to_viewport, DAffine2::IDENTITY); + let [start_viewport, end_viewport] = [start_input, end_input].map(|point| transform.transform_point2(point)); + assert!(start_viewport.abs_diff_eq(expected_start, 1e-10), "expected line to start at {expected_start} not {start_viewport}"); + assert!(end_viewport.abs_diff_eq(expected_end, 1e-10), "expected line to end at {expected_end} not {end_viewport}"); } } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index aafc869238..56aa7897ad 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -528,7 +528,7 @@ impl Fsm for ArtboardToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { ArtboardToolFsmState::Ready { .. } => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Artboard")]), @@ -552,7 +552,7 @@ impl Fsm for ArtboardToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { if let Self::Ready { hovered: false } = self { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); } else { diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 4fba66f7cb..6113b009e3 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -418,7 +418,7 @@ impl Fsm for BrushToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { BrushToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw")]), @@ -430,7 +430,7 @@ impl Fsm for BrushToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index d5a082cc21..d889a44cb3 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -124,7 +124,7 @@ impl Fsm for EyedropperToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, "Sample to Primary"), @@ -138,7 +138,7 @@ impl Fsm for EyedropperToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let cursor = match *self { EyedropperToolFsmState::Ready => MouseCursorIcon::Default, EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary => MouseCursorIcon::None, diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 6a9429e3e9..eb68f707c8 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -145,7 +145,7 @@ impl Fsm for FillToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { FillToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, "Fill with Primary"), @@ -157,7 +157,7 @@ impl Fsm for FillToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index eb862b7fe5..3d7dee4419 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -259,10 +259,14 @@ impl Fsm for FreehandToolFsmState { } (FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => { if let Some(layer) = tool_data.layer { - let transform = document.metadata().transform_to_viewport(layer); - let position = transform.inverse().transform_point2(input.mouse.position); + if !document.metadata().upstream_footprints.contains_key(&layer.to_node()) { + warn!("Freehand tool layer not exist"); + } else { + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.inverse().transform_point2(input.mouse.position); - extend_path_with_next_segment(tool_data, position, true, responses); + extend_path_with_next_segment(tool_data, position, true, responses); + } } FreehandToolFsmState::Drawing @@ -297,7 +301,7 @@ impl Fsm for FreehandToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polyline"), @@ -310,7 +314,7 @@ impl Fsm for FreehandToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 41fdb09df0..636766feb0 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD}; +use crate::consts::{DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD}; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; @@ -29,6 +29,7 @@ pub enum GradientToolMessage { // Tool-specific messages DeleteStop, InsertStop, + InsertStopProxy, PointerDown, PointerMove { constrain_axis: Key }, PointerOutsideViewport { constrain_axis: Key }, @@ -84,6 +85,7 @@ impl<'a> MessageHandler> for Grad PointerMove, Abort, InsertStop, + InsertStopProxy, DeleteStop, ); } @@ -117,6 +119,7 @@ enum GradientToolFsmState { /// Computes the transform from gradient space to viewport space (where gradient space is 0..1) fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { let bounds = document.metadata().nonzero_bounding_box(layer); + println!("Bounds {bounds:?}"); let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let multiplied = document.metadata().transform_to_viewport(layer); @@ -237,6 +240,7 @@ struct GradientToolData { snap_manager: SnapManager, drag_start: DVec2, auto_panning: AutoPanning, + pointer_up_abort: bool, } impl Fsm for GradientToolFsmState { @@ -343,6 +347,7 @@ impl Fsm for GradientToolFsmState { self } (_, GradientToolMessage::InsertStop) => { + println!("GradientToolMessage::InsertStop insert stop --------------------------"); for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { let Some(mut gradient) = get_gradient(layer, &document.network_interface) else { continue }; // TODO: This transform is incorrect. I think this is since it is based on the Footprint which has not been updated yet @@ -351,7 +356,9 @@ impl Fsm for GradientToolFsmState { let (start, end) = (transform.transform_point2(gradient.start), transform.transform_point2(gradient.end)); // Compute the distance from the mouse to the gradient line in viewport space - let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); + let distance = (end - start).normalize_or_zero().perp_dot(mouse - start).abs(); + + println!("> distance {distance} start {start} end {end} mouse {mouse}"); // If click is on the line then insert point if distance < (SELECTION_THRESHOLD * 2.) { @@ -376,6 +383,20 @@ impl Fsm for GradientToolFsmState { self } + // The undo system clears all clear targets only for double click messages after an abort. This hack fixes that. + (_, GradientToolMessage::InsertStopProxy) => { + if tool_data.pointer_up_abort { + let metadata = document.metadata(); + let all_empty = metadata.click_targets.is_empty() && metadata.upstream_footprints.is_empty() && metadata.local_transforms.is_empty(); + assert!(all_empty, "document metada is properly implemented so the InsertStopProxy should be removed {metadata:#?}"); + } + responses.add(DeferMessage::AfterGraphRun { + messages: vec![GradientToolMessage::InsertStop.into()], + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + + self + } (GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => { let mouse = input.mouse.position; tool_data.drag_start = mouse; @@ -493,7 +514,10 @@ impl Fsm for GradientToolFsmState { state } (GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => { - input.mouse.finish_transaction(tool_data.drag_start, responses); + let drag_too_small = tool_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD; + responses.add(if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction }); + tool_data.pointer_up_abort = drag_too_small; + tool_data.snap_manager.cleanup(responses); let was_dragging = tool_data.selected_gradient.is_some(); @@ -519,7 +543,7 @@ impl Fsm for GradientToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Gradient"), @@ -534,7 +558,7 @@ impl Fsm for GradientToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } @@ -693,6 +717,17 @@ mod test_gradient { assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10)); } + #[tokio::test] + async fn double_click_empty_space() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, -5., -5., 105., 105., ModifierKeys::empty()).await; + editor.drag_tool(ToolType::Gradient, 0., 0., 100., 0., ModifierKeys::empty()).await; + editor.pointer_up_double_click(DVec2::new(300., 300.)).await; + let (updated_gradient, _) = get_gradient(&mut editor).await; + assert_eq!(updated_gradient.stops.len(), 2, "Expected 2 stops, found {}", updated_gradient.stops.len()); + } + #[tokio::test] async fn double_click_insert_stop() { let mut editor = EditorTestUtils::create(); @@ -708,7 +743,7 @@ mod test_gradient { assert_eq!(initial_gradient.stops.len(), 2, "Expected 2 stops, found {}", initial_gradient.stops.len()); editor.select_tool(ToolType::Gradient).await; - editor.double_click(DVec2::new(50., 0.)).await; + editor.pointer_up_double_click(DVec2::new(50., 0.)).await; // Check that a new stop has been added let (updated_gradient, _) = get_gradient(&mut editor).await; @@ -802,7 +837,7 @@ mod test_gradient { editor.select_tool(ToolType::Gradient).await; // Add a middle stop at 50% - editor.double_click(DVec2::new(50., 0.)).await; + editor.pointer_up_double_click(DVec2::new(50., 0.)).await; let (initial_gradient, _) = get_gradient(&mut editor).await; assert_eq!(initial_gradient.stops.len(), 3, "Expected 3 stops, found {}", initial_gradient.stops.len()); @@ -877,8 +912,8 @@ mod test_gradient { editor.select_tool(ToolType::Gradient).await; // Add two middle stops - editor.double_click(DVec2::new(25., 0.)).await; - editor.double_click(DVec2::new(75., 0.)).await; + editor.pointer_up_double_click(DVec2::new(25., 0.)).await; + editor.pointer_up_double_click(DVec2::new(75., 0.)).await; let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len()); diff --git a/editor/src/messages/tool/tool_messages/navigate_tool.rs b/editor/src/messages/tool/tool_messages/navigate_tool.rs index 7bbec9d20d..2496729123 100644 --- a/editor/src/messages/tool/tool_messages/navigate_tool.rs +++ b/editor/src/messages/tool/tool_messages/navigate_tool.rs @@ -145,7 +145,7 @@ impl Fsm for NavigateToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { NavigateToolFsmState::Ready | NavigateToolFsmState::ZoomOrClickZooming => HintData(vec![ HintGroup(vec![ @@ -169,7 +169,7 @@ impl Fsm for NavigateToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let cursor = match *self { NavigateToolFsmState::Ready => MouseCursorIcon::ZoomIn, NavigateToolFsmState::Tilting => MouseCursorIcon::Default, diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 00c99bd975..61a9f41078 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1505,9 +1505,9 @@ impl Fsm for PathToolFsmState { tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - let ToolActionMessageContext { document, input, shape_editor, .. } = tool_action_data; + self.update_hints(tool_data, tool_action_data, tool_options, responses); - update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options, input.mouse.position); + let ToolActionMessageContext { document, input, shape_editor, .. } = tool_action_data; let ToolMessage::Path(event) = event else { return self }; @@ -3040,11 +3040,218 @@ impl Fsm for PathToolFsmState { } } - fn update_hints(&self, _responses: &mut VecDeque) { - // Moved logic to update_dynamic_hints + fn update_hints(&self, tool_data: &Self::ToolData, ctx: &ToolActionMessageContext, tool_options: &Self::ToolOptions, responses: &mut VecDeque) { + let ToolActionMessageContext { document, shape_editor, input, .. } = ctx; + + // Condinting based on currently selected segment if it has any one g1 continuous handle + + let hint_data = match self { + PathToolFsmState::Ready => { + // Show point sliding hints only when there is an anchor with colinear handles selected + let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); + let at_least_one_anchor_selected = shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); + let at_least_one_point_selected = shape_editor.selected_points().count() >= 1; + + let mut single_colinear_anchor_selected = false; + if single_anchor_selected { + if let (Some(anchor), Some(layer)) = ( + shape_editor.selected_points().next(), + document.network_interface.selected_nodes().selected_layers(document.metadata()).next(), + ) { + if let Some(vector) = document.network_interface.compute_modified_vector(layer) { + single_colinear_anchor_selected = vector.colinear(*anchor) + } + } + } + + let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]; + let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")]; + + if at_least_one_anchor_selected { + delete_selected_hints.push(HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus()); + delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus()); + } + + if single_colinear_anchor_selected { + drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); + } + + let segment_edit = tool_options.path_editing_mode.segment_editing_mode; + let point_edit = tool_options.path_editing_mode.point_editing_mode; + + let hovering_segment = tool_data.segment.is_some(); + let hovering_point = shape_editor + .find_nearest_visible_point_indices( + &document.network_interface, + input.mouse.position, + SELECTION_THRESHOLD, + tool_options.path_overlay_mode, + &tool_data.frontier_handles_info, + ) + .is_some(); + + let mut hint_data = if hovering_segment { + if segment_edit { + // Hovering a segment in segment editing mode + vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::Lmb, "Insert Point on Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::LmbDrag, "Mold Segment")]), + ] + } else { + // Hovering a segment in point editing mode + vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), + ] + } + } else if hovering_point { + if point_edit { + // Hovering over a point in point editing mode + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Select Point"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + ])] + } else { + // Hovering over a point in segment selection mode (will select a nearby segment) + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + ])] + } + } else { + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), + HintInfo::keys([Key::Control], "Lasso").prepend_plus(), + ])] + }; + + if at_least_one_anchor_selected { + // TODO: Dynamically show either "Smooth" or "Sharp" based on the current state + hint_data.push(HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"), + HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"), + HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"), + ])); + } + + if at_least_one_point_selected { + let mut groups = vec![ + HintGroup(drag_selected_hints), + HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), + HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), + HintGroup(delete_selected_hints), + ]; + hint_data.append(&mut groups); + } + + HintData(hint_data) + } + PathToolFsmState::Dragging(dragging_state) => { + let colinear = dragging_state.colinear; + let mut dragging_hint_data = HintData(Vec::new()); + dragging_hint_data + .0 + .push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); + + let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); + let toggle_group = match dragging_state.point_select_state { + PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => { + let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")]; + hints.push(HintInfo::keys( + [Key::KeyC], + if colinear == ManipulatorAngle::Colinear { + "Break Colinear Handles" + } else { + "Make Handles Colinear" + }, + )); + hints + } + PointSelectState::Anchor => Vec::new(), + }; + let hold_group = match dragging_state.point_select_state { + PointSelectState::HandleNoPair => { + let mut hints = vec![]; + if colinear != ManipulatorAngle::Free { + hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); + } + hints.push(HintInfo::keys([Key::Shift], "15° Increments")); + hints.push(HintInfo::keys([Key::Control], "Lock Angle")); + hints.push(drag_anchor); + hints + } + PointSelectState::HandleWithPair => { + let mut hints = vec![]; + if colinear != ManipulatorAngle::Free { + hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); + } + hints.push(HintInfo::keys([Key::Shift], "15° Increments")); + hints.push(HintInfo::keys([Key::Control], "Lock Angle")); + hints.push(drag_anchor); + hints + } + PointSelectState::Anchor => Vec::new(), + }; + + if !toggle_group.is_empty() { + dragging_hint_data.0.push(HintGroup(toggle_group)); + } + + if !hold_group.is_empty() { + dragging_hint_data.0.push(HintGroup(hold_group)); + } + + if tool_data.molding_segment { + let mut has_colinear_anchors = false; + + if let Some(segment) = &tool_data.segment { + let handle1 = HandleId::primary(segment.segment()); + let handle2 = HandleId::end(segment.segment()); + + if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) { + let other_handle1 = vector.other_colinear_handle(handle1); + let other_handle2 = vector.other_colinear_handle(handle2); + if other_handle1.is_some() || other_handle2.is_some() { + has_colinear_anchors = true; + } + }; + } + + let handles_stored = if let Some(other_handles) = tool_data.temporary_adjacent_handles_while_molding { + other_handles[0].is_some() || other_handles[1].is_some() + } else { + false + }; + + let molding_disable_possible = has_colinear_anchors || handles_stored; + + let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; + + if molding_disable_possible { + molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")])); + } + + HintData(molding_hints) + } else { + dragging_hint_data + } + } + PathToolFsmState::Drawing { .. } => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), + ]), + ]), + PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), + }; + responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } @@ -3260,220 +3467,3 @@ fn calculate_adjacent_anchor_tangent(currently_dragged_handle: ManipulatorPointI _ => (None, None), } } - -fn update_dynamic_hints( - state: PathToolFsmState, - responses: &mut VecDeque, - shape_editor: &mut ShapeState, - document: &DocumentMessageHandler, - tool_data: &PathToolData, - tool_options: &PathToolOptions, - position: DVec2, -) { - // Condinting based on currently selected segment if it has any one g1 continuous handle - - let hint_data = match state { - PathToolFsmState::Ready => { - // Show point sliding hints only when there is an anchor with colinear handles selected - let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); - let at_least_one_anchor_selected = shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); - let at_least_one_point_selected = shape_editor.selected_points().count() >= 1; - - let mut single_colinear_anchor_selected = false; - if single_anchor_selected { - if let (Some(anchor), Some(layer)) = ( - shape_editor.selected_points().next(), - document.network_interface.selected_nodes().selected_layers(document.metadata()).next(), - ) { - if let Some(vector) = document.network_interface.compute_modified_vector(layer) { - single_colinear_anchor_selected = vector.colinear(*anchor) - } - } - } - - let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]; - let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")]; - - if at_least_one_anchor_selected { - delete_selected_hints.push(HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus()); - delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus()); - } - - if single_colinear_anchor_selected { - drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); - } - - let segment_edit = tool_options.path_editing_mode.segment_editing_mode; - let point_edit = tool_options.path_editing_mode.point_editing_mode; - - let hovering_segment = tool_data.segment.is_some(); - let hovering_point = shape_editor - .find_nearest_visible_point_indices( - &document.network_interface, - position, - SELECTION_THRESHOLD, - tool_options.path_overlay_mode, - &tool_data.frontier_handles_info, - ) - .is_some(); - - let mut hint_data = if hovering_segment { - if segment_edit { - // Hovering a segment in segment editing mode - vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::Lmb, "Insert Point on Segment")]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::LmbDrag, "Mold Segment")]), - ] - } else { - // Hovering a segment in point editing mode - vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), - HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), - ] - } - } else if hovering_point { - if point_edit { - // Hovering over a point in point editing mode - vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::Lmb, "Select Point"), - HintInfo::keys([Key::Shift], "Extend").prepend_plus(), - ])] - } else { - // Hovering over a point in segment selection mode (will select a nearby segment) - vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), - HintInfo::keys([Key::Shift], "Extend").prepend_plus(), - ])] - } - } else { - vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), - HintInfo::keys([Key::Control], "Lasso").prepend_plus(), - ])] - }; - - if at_least_one_anchor_selected { - // TODO: Dynamically show either "Smooth" or "Sharp" based on the current state - hint_data.push(HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"), - HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"), - HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"), - ])); - } - - if at_least_one_point_selected { - let mut groups = vec![ - HintGroup(drag_selected_hints), - HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), - HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), - HintGroup(delete_selected_hints), - ]; - hint_data.append(&mut groups); - } - - HintData(hint_data) - } - PathToolFsmState::Dragging(dragging_state) => { - let colinear = dragging_state.colinear; - let mut dragging_hint_data = HintData(Vec::new()); - dragging_hint_data - .0 - .push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); - - let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); - let toggle_group = match dragging_state.point_select_state { - PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => { - let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")]; - hints.push(HintInfo::keys( - [Key::KeyC], - if colinear == ManipulatorAngle::Colinear { - "Break Colinear Handles" - } else { - "Make Handles Colinear" - }, - )); - hints - } - PointSelectState::Anchor => Vec::new(), - }; - let hold_group = match dragging_state.point_select_state { - PointSelectState::HandleNoPair => { - let mut hints = vec![]; - if colinear != ManipulatorAngle::Free { - hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); - } - hints.push(HintInfo::keys([Key::Shift], "15° Increments")); - hints.push(HintInfo::keys([Key::Control], "Lock Angle")); - hints.push(drag_anchor); - hints - } - PointSelectState::HandleWithPair => { - let mut hints = vec![]; - if colinear != ManipulatorAngle::Free { - hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); - } - hints.push(HintInfo::keys([Key::Shift], "15° Increments")); - hints.push(HintInfo::keys([Key::Control], "Lock Angle")); - hints.push(drag_anchor); - hints - } - PointSelectState::Anchor => Vec::new(), - }; - - if !toggle_group.is_empty() { - dragging_hint_data.0.push(HintGroup(toggle_group)); - } - - if !hold_group.is_empty() { - dragging_hint_data.0.push(HintGroup(hold_group)); - } - - if tool_data.molding_segment { - let mut has_colinear_anchors = false; - - if let Some(segment) = &tool_data.segment { - let handle1 = HandleId::primary(segment.segment()); - let handle2 = HandleId::end(segment.segment()); - - if let Some(vector) = document.network_interface.compute_modified_vector(segment.layer()) { - let other_handle1 = vector.other_colinear_handle(handle1); - let other_handle2 = vector.other_colinear_handle(handle2); - if other_handle1.is_some() || other_handle2.is_some() { - has_colinear_anchors = true; - } - }; - } - - let handles_stored = if let Some(other_handles) = tool_data.temporary_adjacent_handles_while_molding { - other_handles[0].is_some() || other_handles[1].is_some() - } else { - false - }; - - let molding_disable_possible = has_colinear_anchors || handles_stored; - - let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; - - if molding_disable_possible { - molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")])); - } - - HintData(molding_hints) - } else { - dragging_hint_data - } - } - PathToolFsmState::Drawing { .. } => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), - HintInfo::keys([Key::Shift], "Extend").prepend_plus(), - HintInfo::keys([Key::Alt], "Subtract").prepend_plus(), - ]), - ]), - PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), - }; - responses.add(FrontendMessage::UpdateInputHints { hint_data }); -} diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 00ec54db8d..70be11b24b 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2164,7 +2164,7 @@ impl Fsm for PenToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { PenToolFsmState::Ready | PenToolFsmState::GRSHandle => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, "Draw Path"), @@ -2226,7 +2226,7 @@ impl Fsm for PenToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 31bd2c3a4b..2e337116b3 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -6,28 +6,29 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; -use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; -use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; -use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; +use crate::messages::portfolio::document::utility_types::network_interface::{NodeNetworkInterface, ShallowestSelectionIter}; use crate::messages::preferences::SelectionMode; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::measure; -use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget}; +use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType}; use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage}; +use crate::messages::tool::tool_messages::select_tool::options::NestedSelectionBehavior; use bezier_rs::Subpath; use glam::DMat2; -use graph_craft::document::NodeId; -use graphene_std::path_bool::BooleanOperation; use graphene_std::renderer::Quad; use graphene_std::renderer::Rect; use graphene_std::transform::ReferencePoint; -use std::fmt; + +mod drag_state; +mod duplicate; +pub mod options; +use drag_state::*; #[derive(Default, ExtractField)] pub struct SelectTool { @@ -35,36 +36,6 @@ pub struct SelectTool { tool_data: SelectToolData, } -#[allow(dead_code)] -#[derive(Default)] -pub struct SelectOptions { - nested_selection_behavior: NestedSelectionBehavior, -} - -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum SelectOptionsUpdate { - NestedSelectionBehavior(NestedSelectionBehavior), - PivotGizmoType(PivotGizmoType), - TogglePivotGizmoType(bool), - TogglePivotPinned, -} - -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum NestedSelectionBehavior { - #[default] - Shallowest, - Deepest, -} - -impl fmt::Display for NestedSelectionBehavior { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NestedSelectionBehavior::Deepest => write!(f, "Deep Select"), - NestedSelectionBehavior::Shallowest => write!(f, "Shallow Select"), - } - } -} - #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub struct SelectToolPointerKeys { pub axis_align: Key, @@ -95,7 +66,7 @@ pub enum SelectToolMessage { Enter, PointerMove(SelectToolPointerKeys), PointerOutsideViewport(SelectToolPointerKeys), - SelectOptions(SelectOptionsUpdate), + SelectOptions(options::SelectOptionsUpdate), SetPivot { position: ReferencePoint, }, @@ -121,200 +92,19 @@ impl ToolMetadata for SelectTool { } } -impl SelectTool { - fn deep_selection_widget(&self) -> WidgetHolder { - let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest] - .iter() - .map(|mode| { - MenuListEntry::new(format!("{mode:?}")) - .label(mode.to_string()) - .on_commit(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into()) - }) - .collect(); - - DropdownInput::new(vec![layer_selection_behavior_entries]) - .selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest) as u32)) - .tooltip( - "Selection Mode\n\ - \n\ - Shallow Select: clicks initially select the least-nested layers and double clicks drill deeper into the folder hierarchy.\n\ - Deep Select: clicks directly select the most-nested layers in the folder hierarchy.", - ) - .widget_holder() - } - - fn alignment_widgets(&self, disabled: bool) -> impl Iterator + use<> { - [AlignAxis::X, AlignAxis::Y] - .into_iter() - .flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)]) - .map(move |(axis, aggregate)| { - let (icon, tooltip) = match (axis, aggregate) { - (AlignAxis::X, AlignAggregate::Min) => ("AlignLeft", "Align Left"), - (AlignAxis::X, AlignAggregate::Center) => ("AlignHorizontalCenter", "Align Horizontal Center"), - (AlignAxis::X, AlignAggregate::Max) => ("AlignRight", "Align Right"), - (AlignAxis::Y, AlignAggregate::Min) => ("AlignTop", "Align Top"), - (AlignAxis::Y, AlignAggregate::Center) => ("AlignVerticalCenter", "Align Vertical Center"), - (AlignAxis::Y, AlignAggregate::Max) => ("AlignBottom", "Align Bottom"), - }; - IconButton::new(icon, 24) - .tooltip(tooltip) - .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()) - .disabled(disabled) - .widget_holder() - }) - } - - fn flip_widgets(&self, disabled: bool) -> impl Iterator + use<> { - [(FlipAxis::X, "Horizontal"), (FlipAxis::Y, "Vertical")].into_iter().map(move |(flip_axis, name)| { - IconButton::new("Flip".to_string() + name, 24) - .tooltip("Flip ".to_string() + name) - .on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into()) - .disabled(disabled) - .widget_holder() - }) - } - - fn turn_widgets(&self, disabled: bool) -> impl Iterator + use<> { - [(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")] - .into_iter() - .map(move |(degrees, icon, name)| { - IconButton::new(icon, 24) - .tooltip(name) - .on_update(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into()) - .disabled(disabled) - .widget_holder() - }) - } - - fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { - let list = ::list(); - list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| { - let mut tooltip = info.label.to_string(); - if let Some(doc) = info.docstring.as_deref() { - tooltip.push_str("\n\n"); - tooltip.push_str(doc); - } - IconButton::new(info.icon.as_deref().unwrap(), 24) - .tooltip(tooltip) - .disabled(selected_count == 0) - .on_update(move |_| { - let group_folder_type = GroupFolderType::BooleanOperation(*operation); - DocumentMessage::GroupSelectedLayers { group_folder_type }.into() - }) - .widget_holder() - }) - } -} - -impl LayoutHolder for SelectTool { - fn layout(&self) -> Layout { - let mut widgets = Vec::new(); - - // Select mode (Deep/Shallow) - widgets.push(self.deep_selection_widget()); - - // Pivot gizmo type (checkbox + dropdown for pivot/origin) - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.extend(pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Select)); - - if self.tool_data.pivot_gizmo.state.is_pivot_type() { - // Nine-position reference point widget - widgets.push(Separator::new(SeparatorType::Related).widget_holder()); - widgets.push(pivot_reference_point_widget( - self.tool_data.selected_layers_count == 0 || !self.tool_data.pivot_gizmo.state.is_pivot(), - self.tool_data.pivot_gizmo.pivot.to_pivot_position(), - PivotToolSource::Select, - )); - - // Pivot pin button - widgets.push(Separator::new(SeparatorType::Related).widget_holder()); - - let pin_active = self.tool_data.pivot_gizmo.pin_active(); - let pin_enabled = self.tool_data.pivot_gizmo.pivot.old_pivot_position == ReferencePoint::None && !self.tool_data.pivot_gizmo.state.disabled; - - if pin_active || pin_enabled { - widgets.push(pin_pivot_widget(pin_active, pin_enabled, PivotToolSource::Select)); - } - } - - // Align - let disabled = self.tool_data.selected_layers_count < 2; - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.extend(self.alignment_widgets(disabled)); - // widgets.push( - // PopoverButton::new() - // .popover_layout(vec![ - // LayoutGroup::Row { - // widgets: vec![TextLabel::new("Align").bold(true).widget_holder()], - // }, - // LayoutGroup::Row { - // widgets: vec![TextLabel::new("Coming soon").widget_holder()], - // }, - // ]) - // .disabled(disabled) - // .widget_holder(), - // ); - - // Flip - let disabled = self.tool_data.selected_layers_count == 0; - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.extend(self.flip_widgets(disabled)); - - // Turn - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.extend(self.turn_widgets(disabled)); - - // Boolean - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.extend(self.boolean_widgets(self.tool_data.selected_layers_count)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} - #[message_handler_data] impl<'a> MessageHandler> for SelectTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { - let mut redraw_reference_pivot = false; - if let ToolMessage::Select(SelectToolMessage::SelectOptions(ref option_update)) = message { - match option_update { - SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => { - self.tool_data.nested_selection_behavior = *nested_selection_behavior; - responses.add(ToolMessage::UpdateHints); - } - SelectOptionsUpdate::PivotGizmoType(gizmo_type) => { - if !self.tool_data.pivot_gizmo.state.disabled { - self.tool_data.pivot_gizmo.state.gizmo_type = *gizmo_type; - responses.add(ToolMessage::UpdateHints); - let pivot_gizmo = self.tool_data.pivot_gizmo(); - responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo }); - responses.add(NodeGraphMessage::RunDocumentGraph); - redraw_reference_pivot = true; - } - } - SelectOptionsUpdate::TogglePivotGizmoType(state) => { - self.tool_data.pivot_gizmo.state.disabled = !state; - responses.add(ToolMessage::UpdateHints); - responses.add(NodeGraphMessage::RunDocumentGraph); - redraw_reference_pivot = true; - } - - SelectOptionsUpdate::TogglePivotPinned => { - self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned; - responses.add(ToolMessage::UpdateHints); - responses.add(NodeGraphMessage::RunDocumentGraph); - redraw_reference_pivot = true; - } - } + self.update_tool_options(option_update, responses); } self.fsm_state.process_event(message, &mut self.tool_data, context, &(), responses, false); - if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_pivot { + if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.pivot_changed { // Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool) self.send_layout(responses, LayoutTarget::ToolOptions); - self.tool_data.selected_layers_changed = false; + self.tool_data.pivot_changed = false; } } @@ -327,7 +117,7 @@ impl<'a> MessageHandler> for Sele ); let additional = match self.fsm_state { - SelectToolFsmState::Ready { .. } => actions!(SelectToolMessageDiscriminant; DragStart), + SelectToolFsmState::Ready => actions!(SelectToolMessageDiscriminant; DragStart), _ => actions!(SelectToolMessageDiscriminant; DragStop), }; common.extend(additional); @@ -348,9 +138,7 @@ impl ToolTransition for SelectTool { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum SelectToolFsmState { - Ready { - selection: NestedSelectionBehavior, - }, + Ready, Drawing { selection_shape: SelectionShapeType, has_drawn: bool, @@ -372,35 +160,36 @@ enum SelectToolFsmState { impl Default for SelectToolFsmState { fn default() -> Self { - let selection = NestedSelectionBehavior::Deepest; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } } #[derive(Clone, Debug, Default)] struct SelectToolData { - drag_start: ViewportPosition, - drag_current: ViewportPosition, + drag: DragState, + duplicates: duplicate::DuplcateState, + + // select lasso_polygon: Vec, - selection_mode: Option, - layers_dragging: Vec, // Unordered, often used as temporary buffer - ordered_layers: Vec, // Ordered list of layers layer_selected_on_start: Option, select_single_layer: Option, + + layers_dragging: Vec, // Unordered, often used as temporary buffer + + ordered_layers: Vec, // Ordered list of layers + axis_align: bool, - non_duplicated_layers: Option>, bounding_box_manager: Option, snap_manager: SnapManager, cursor: MouseCursorIcon, pivot_gizmo: PivotGizmo, - pivot_gizmo_start: Option, pivot_gizmo_shift: Option, compass_rose: CompassRose, line_center: DVec2, skew_edge: EdgeBool, - nested_selection_behavior: NestedSelectionBehavior, + nested_selection_behavior: options::NestedSelectionBehavior, selected_layers_count: usize, - selected_layers_changed: bool, + pivot_changed: bool, snap_candidates: Vec, auto_panning: AutoPanning, } @@ -419,37 +208,6 @@ impl SelectToolData { } } - pub fn selection_quad(&self) -> Quad { - let bbox = self.selection_box(); - Quad::from_box(bbox) - } - - pub fn calculate_selection_mode_from_direction(&mut self) -> SelectionMode { - let bbox: [DVec2; 2] = self.selection_box(); - let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2); - - if self.selection_mode.is_none() && above_threshold { - let mode = if bbox[1].x < bbox[0].x { - SelectionMode::Touched - } else { - // This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area - SelectionMode::Enclosed - }; - self.selection_mode = Some(mode); - } - - self.selection_mode.unwrap_or(SelectionMode::Touched) - } - - pub fn selection_box(&self) -> [DVec2; 2] { - if self.drag_current == self.drag_start { - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - [self.drag_start - tolerance, self.drag_start + tolerance] - } else { - [self.drag_start, self.drag_current] - } - } - pub fn intersect_lasso_no_artboards(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> Vec { if self.lasso_polygon.len() < 2 { return Vec::new(); @@ -466,101 +224,6 @@ impl SelectToolData { document.is_layer_fully_inside_polygon(layer, input, polygon) } - /// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated. - fn start_duplicates(&mut self, document: &mut DocumentMessageHandler, responses: &mut VecDeque) { - self.non_duplicated_layers = Some(self.layers_dragging.clone()); - let mut new_dragging = Vec::new(); - - // Get the shallowest unique layers and sort by their index relative to parent for ordered processing - let mut layers = document.network_interface.shallowest_unique_layers(&[]).collect::>(); - - layers.sort_by_key(|layer| { - let Some(parent) = layer.parent(document.metadata()) else { return usize::MAX }; - DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent) - }); - - for layer in layers.into_iter().rev() { - let Some(parent) = layer.parent(document.metadata()) else { continue }; - - // Moves the layer back to its starting position. - responses.add(GraphOperationMessage::TransformChange { - layer, - transform: DAffine2::from_translation(self.drag_start - self.drag_current), - transform_in: TransformIn::Viewport, - skip_rerender: true, - }); - - // Copy the layer - let mut copy_ids = HashMap::new(); - let node_id = layer.to_node(); - copy_ids.insert(node_id, NodeId(0)); - - document - .network_interface - .upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::LayerChildrenUpstreamFlow) - .enumerate() - .for_each(|(index, node_id)| { - copy_ids.insert(node_id, NodeId((index + 1) as u64)); - }); - - let nodes = document.network_interface.copy_nodes(©_ids, &[]).collect::>(); - - let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent); - - let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); - - let layer_id = *new_ids.get(&NodeId(0)).expect("Node Id 0 should be a layer"); - let layer = LayerNodeIdentifier::new_unchecked(layer_id); - new_dragging.push(layer); - responses.add(NodeGraphMessage::AddNodes { nodes, new_ids }); - responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index }); - } - let nodes = new_dragging.iter().map(|layer| layer.to_node()).collect(); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); - responses.add(NodeGraphMessage::RunDocumentGraph); - self.layers_dragging = new_dragging; - } - - /// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated. - fn stop_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { - let Some(original) = self.non_duplicated_layers.take() else { - return; - }; - - // Delete the duplicated layers - for layer in document.network_interface.shallowest_unique_layers(&[]) { - responses.add(NodeGraphMessage::DeleteNodes { - node_ids: vec![layer.to_node()], - delete_children: true, - }); - } - - for &layer in &original { - responses.add(GraphOperationMessage::TransformChange { - layer, - transform: DAffine2::from_translation(self.drag_current - self.drag_start), - transform_in: TransformIn::Viewport, - skip_rerender: true, - }); - } - let nodes = original - .iter() - .filter_map(|layer| { - if *layer != LayerNodeIdentifier::ROOT_PARENT { - Some(layer.to_node()) - } else { - log::error!("ROOT_PARENT cannot be part of non_duplicated_layers"); - None - } - }) - .collect(); - responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); - responses.add(NodeGraphMessage::RunDocumentGraph); - responses.add(NodeGraphMessage::SelectedNodesUpdated); - responses.add(NodeGraphMessage::SendGraph); - self.layers_dragging = original; - } - fn state_from_pivot_gizmo(&self, mouse: DVec2) -> Option { match self.pivot_gizmo.state.gizmo_type { PivotGizmoType::Pivot if self.pivot_gizmo.state.is_pivot() => self.pivot_gizmo.pivot.is_over(mouse).then_some(SelectToolFsmState::DraggingPivot), @@ -593,7 +256,7 @@ impl Fsm for SelectToolFsmState { tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count(); - tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count; + tool_data.pivot_changed = selected_layers_count != tool_data.selected_layers_count; tool_data.selected_layers_count = selected_layers_count; // Outline selected layers, but not artboards @@ -841,10 +504,7 @@ impl Fsm for SelectToolFsmState { tool_data.pivot_gizmo.pivot.recalculate_pivot(document); let pivot = draw_pivot.then_some(tool_data.pivot_gizmo.pivot.pivot).flatten(); if let Some(pivot) = pivot { - let offset = tool_data - .pivot_gizmo_start - .map(|offset| tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - offset).unwrap_or_default()) - .unwrap_or_default(); + let offset = tool_data.drag.total_drag_delta_viewport(document.metadata()); let shift = tool_data.pivot_gizmo_shift.unwrap_or_default(); overlay_context.pivot(pivot + offset + shift, angle); } @@ -895,12 +555,11 @@ impl Fsm for SelectToolFsmState { } if axis_state.is_none_or(|(axis, _)| !axis.is_constraint()) && tool_data.axis_align { - let mouse_position = mouse_position - tool_data.drag_start; let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); - let angle = -mouse_position.angle_to(DVec2::X); + let angle = -tool_data.drag.total_drag_delta_viewport(document.metadata()).angle_to(DVec2::X); let snapped_angle = (angle / snap_resolution).round() * snap_resolution; - let extension = tool_data.drag_current - tool_data.drag_start; + let extension = tool_data.drag.total_drag_delta_viewport(document.metadata()); let origin = compass_center - extension; let viewport_diagonal = input.viewport_bounds.size().length(); @@ -923,13 +582,9 @@ impl Fsm for SelectToolFsmState { // Check if the tool is in selection mode if let Self::Drawing { selection_shape, .. } = self { // Get the updated selection box bounds - let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]); + let quad = Quad::from_box(tool_data.drag.start_current_viewport(document.metadata())); - let current_selection_mode = match tool_action_data.preferences.get_selection_mode() { - SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(), - SelectionMode::Touched => SelectionMode::Touched, - SelectionMode::Enclosed => SelectionMode::Enclosed, - }; + let current_selection_mode = tool_data.drag.update_selection_mode(document.metadata(), tool_action_data.preferences); // Draw outline visualizations on the layers to be selected let intersected_layers = match selection_shape { @@ -975,11 +630,9 @@ impl Fsm for SelectToolFsmState { } if let Self::Dragging { .. } = self { - let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]); - let document_start = document.metadata().document_to_viewport.inverse().transform_point2(quad.top_left()); - let document_current = document.metadata().document_to_viewport.inverse().transform_point2(quad.bottom_right()); + let quad = Quad::from_box(tool_data.drag.start_current_viewport(document.metadata())); - overlay_context.translation_box(document_current - document_start, quad, None); + overlay_context.translation_box(tool_data.drag.total_drag_delta_document(), quad, None); } self @@ -996,7 +649,7 @@ impl Fsm for SelectToolFsmState { self } ( - SelectToolFsmState::Ready { .. }, + SelectToolFsmState::Ready, SelectToolMessage::DragStart { extend_selection, remove_from_selection, @@ -1005,9 +658,8 @@ impl Fsm for SelectToolFsmState { .. }, ) => { - tool_data.drag_start = input.mouse.position; - tool_data.drag_current = input.mouse.position; - tool_data.selection_mode = None; + tool_data.drag = DragState::new(input, document.metadata()); + tool_data.duplicates.reset(); let mut selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); let intersection_list = document.click_list(input).collect::>(); @@ -1066,8 +718,6 @@ impl Fsm for SelectToolFsmState { (axis_state.unwrap_or_default(), axis_state.is_some()) }; - tool_data.pivot_gizmo_start = Some(tool_data.drag_current); - SelectToolFsmState::Dragging { axis, using_compass, @@ -1107,8 +757,6 @@ impl Fsm for SelectToolFsmState { responses.add(DocumentMessage::StartTransaction); - tool_data.pivot_gizmo_start = Some(tool_data.drag_current); - SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false, @@ -1121,15 +769,13 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Drawing { selection_shape, has_drawn: false } } }; - tool_data.non_duplicated_layers = None; state } (SelectToolFsmState::DraggingPivot, SelectToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } ( SelectToolFsmState::Dragging { @@ -1144,20 +790,19 @@ impl Fsm for SelectToolFsmState { if !has_dragged { responses.add(ToolMessage::UpdateHints); } - if input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_none() { - tool_data.start_duplicates(document, responses); - } else if !input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_some() { - tool_data.stop_duplicates(document, responses); - } + tool_data + .duplicates + .set_duplicating(input.keyboard.key(modifier_keys.duplicate), &mut tool_data.layers_dragging, document, &tool_data.drag, responses); tool_data.axis_align = input.keyboard.key(modifier_keys.axis_align); // Ignore the non duplicated layers if the current layers have not spawned yet. let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_targets(layer).is_some()); - let ignore = tool_data.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging); + let ignore = tool_data.duplicates.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging); let snap_data = SnapData::ignore(document, input, ignore); - let (start, current) = (tool_data.drag_start, tool_data.drag_current); + let [start, current] = tool_data.drag.start_current_viewport(document.metadata()); + let e0 = tool_data .bounding_box_manager .as_ref() @@ -1172,7 +817,7 @@ impl Fsm for SelectToolFsmState { }; // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 - for layer in document.network_interface.shallowest_unique_layers(&[]) { + for layer in ShallowestSelectionIter::new(document.metadata(), &tool_data.layers_dragging) { responses.add_front(GraphOperationMessage::TransformChange { layer, transform: DAffine2::from_translation(mouse_delta), @@ -1180,7 +825,8 @@ impl Fsm for SelectToolFsmState { skip_rerender: false, }); } - tool_data.drag_current += mouse_delta; + + tool_data.drag.offset_viewport(mouse_delta, document.metadata()); // Auto-panning let messages = [ @@ -1240,7 +886,7 @@ impl Fsm for SelectToolFsmState { responses, bounds, &mut tool_data.layers_dragging, - tool_data.drag_start, + tool_data.drag.start_viewport(document.metadata()), input.mouse.position, input.keyboard.key(Key::Shift), ToolType::Select, @@ -1271,11 +917,11 @@ impl Fsm for SelectToolFsmState { responses.add(ToolMessage::UpdateHints); } - tool_data.drag_current = input.mouse.position; + tool_data.drag.set_current(input, document.metadata()); responses.add(OverlaysMessage::Draw); if selection_shape == SelectionShapeType::Lasso { - extend_lasso(&mut tool_data.lasso_polygon, tool_data.drag_current); + extend_lasso(&mut tool_data.lasso_polygon, tool_data.drag.current_viewport(document.metadata())); } // Auto-panning @@ -1287,7 +933,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Drawing { selection_shape, has_drawn: true } } - (SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => { + (SelectToolFsmState::Ready, SelectToolMessage::PointerMove(_)) => { let dragging_bounds = tool_data .bounding_box_manager .as_mut() @@ -1312,8 +958,7 @@ impl Fsm for SelectToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor }); } - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } ( SelectToolFsmState::Dragging { @@ -1326,10 +971,7 @@ impl Fsm for SelectToolFsmState { SelectToolMessage::PointerOutsideViewport(_), ) => { // Auto-panning - if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { - tool_data.drag_current += shift; - tool_data.drag_start += shift; - } + tool_data.auto_panning.shift_viewport(input, responses); SelectToolFsmState::Dragging { axis, @@ -1358,9 +1000,7 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport(_)) => { // Auto-panning - if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { - tool_data.drag_start += shift; - } + tool_data.auto_panning.shift_viewport(input, responses); self } @@ -1381,7 +1021,7 @@ impl Fsm for SelectToolFsmState { if !has_dragged && input.keyboard.key(remove_from_selection) && tool_data.layer_selected_on_start.is_none() { // When you click on the layer with remove from selection key (shift) pressed, we deselect all nodes that are children. - let quad = tool_data.selection_quad(); + let quad = Quad::from_box(tool_data.drag.expanded_selection_box_viewport(document.metadata())); let intersection = document.intersect_quad_no_artboards(quad, input); if let Some(path) = intersection.last() { @@ -1440,31 +1080,26 @@ impl Fsm for SelectToolFsmState { tool_data.snap_manager.cleanup(responses); tool_data.select_single_layer = None; - if let Some(start) = tool_data.pivot_gizmo_start { - let offset = tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - start).unwrap_or_default(); + if tool_data.pivot_gizmo.pivot_disconnected() { + let offset = tool_data.drag.total_drag_delta_viewport(document.metadata()); if let Some(v) = tool_data.pivot_gizmo.pivot.pivot.as_mut() { *v += offset; } } - tool_data.pivot_gizmo_start = None; let pivot_gizmo = tool_data.pivot_gizmo(); responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo }); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } ( SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. } | SelectToolFsmState::RotatingBounds | SelectToolFsmState::DraggingPivot, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter, ) => { - let drag_too_small = input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON; - let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction }; - let pivot_gizmo = tool_data.pivot_gizmo(); responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo }); - responses.add(response); + input.mouse.finish_transaction(tool_data.drag.start_viewport(document.metadata()), responses); tool_data.axis_align = false; tool_data.snap_manager.cleanup(responses); @@ -1475,16 +1110,12 @@ impl Fsm for SelectToolFsmState { } } - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } (SelectToolFsmState::Drawing { selection_shape, .. }, SelectToolMessage::DragStop { remove_from_selection }) => { - let quad = tool_data.selection_quad(); + let quad = Quad::from_box(tool_data.drag.start_current_viewport(document.metadata())); - let selection_mode = match tool_action_data.preferences.get_selection_mode() { - SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(), - selection_mode => selection_mode, - }; + let selection_mode = tool_data.drag.update_selection_mode(document.metadata(), tool_action_data.preferences); let intersection: Vec = match selection_shape { SelectionShapeType::Box => document.intersect_quad_no_artboards(quad, input).collect(), @@ -1549,10 +1180,9 @@ impl Fsm for SelectToolFsmState { responses.add(OverlaysMessage::Draw); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } - (SelectToolFsmState::Ready { .. }, SelectToolMessage::Enter) => { + (SelectToolFsmState::Ready, SelectToolMessage::Enter) => { let selected_nodes = document.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(document.metadata()); @@ -1564,8 +1194,7 @@ impl Fsm for SelectToolFsmState { } } - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } (SelectToolFsmState::Dragging { .. }, SelectToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); @@ -1574,8 +1203,7 @@ impl Fsm for SelectToolFsmState { tool_data.lasso_polygon.clear(); responses.add(OverlaysMessage::Draw); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } (_, SelectToolMessage::Abort) => { tool_data.layers_dragging.retain(|layer| { @@ -1595,8 +1223,7 @@ impl Fsm for SelectToolFsmState { tool_data.lasso_polygon.clear(); responses.add(OverlaysMessage::Draw); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::Ready { selection } + SelectToolFsmState::Ready } (_, SelectToolMessage::SetPivot { position }) => { responses.add(DocumentMessage::StartTransaction); @@ -1650,28 +1277,14 @@ impl Fsm for SelectToolFsmState { } } - fn standard_tool_messages(&self, message: &ToolMessage, responses: &mut VecDeque) -> bool { - // Check for standard hits or cursor events - match message { - ToolMessage::UpdateHints => { - self.update_hints(responses); - true - } - ToolMessage::UpdateCursor => { - self.update_cursor(responses); - true - } - _ => false, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, tool_data: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { match self { - SelectToolFsmState::Ready { selection } => { + SelectToolFsmState::Ready => { + let selection = tool_data.nested_selection_behavior; let hint_data = HintData(vec![ HintGroup({ let mut hints = vec![HintInfo::mouse(MouseMotion::Lmb, "Select Object"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]; - if *selection == NestedSelectionBehavior::Shallowest { + if selection == NestedSelectionBehavior::Shallowest { hints.extend([HintInfo::keys([Key::Accel], "Deepest").prepend_plus(), HintInfo::mouse(MouseMotion::LmbDouble, "Deepen")]); } hints @@ -1752,7 +1365,7 @@ impl Fsm for SelectToolFsmState { } } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool/drag_state.rs b/editor/src/messages/tool/tool_messages/select_tool/drag_state.rs new file mode 100644 index 0000000000..e99b5565b8 --- /dev/null +++ b/editor/src/messages/tool/tool_messages/select_tool/drag_state.rs @@ -0,0 +1,84 @@ +use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, SELECTION_TOLERANCE}; +use crate::messages::{portfolio::document::utility_types::document_metadata::DocumentMetadata, preferences::SelectionMode, tool::tool_messages::tool_prelude::*}; +/// Represents the current drag in progress +#[derive(Clone, Debug, Default)] +pub struct DragState { + pub start_document: DVec2, + pub current_document: DVec2, + /// Selection mode is set when the drag exceeds a certain distance. Once resolved, the selection mode cannot change. + resolved_selection_mode: Option, +} + +impl DragState { + pub fn new(input: &InputPreprocessorMessageHandler, metadata: &DocumentMetadata) -> Self { + let document_mouse = metadata.document_to_viewport.inverse().transform_point2(input.mouse.position); + Self { + start_document: document_mouse, + current_document: document_mouse, + resolved_selection_mode: None, + } + } + pub fn set_current(&mut self, input: &InputPreprocessorMessageHandler, metadata: &DocumentMetadata) { + self.current_document = metadata.document_to_viewport.inverse().transform_point2(input.mouse.position); + } + + pub fn offset_viewport(&mut self, offset: DVec2, metadata: &DocumentMetadata) { + self.current_document = self.current_document + metadata.document_to_viewport.inverse().transform_vector2(offset); + } + + pub fn start_viewport(&self, metadata: &DocumentMetadata) -> DVec2 { + metadata.document_to_viewport.transform_point2(self.start_document) + } + + pub fn current_viewport(&self, metadata: &DocumentMetadata) -> DVec2 { + metadata.document_to_viewport.transform_point2(self.current_document) + } + + pub fn start_current_viewport(&self, metadata: &DocumentMetadata) -> [DVec2; 2] { + [self.start_viewport(metadata), self.current_viewport(metadata)] + } + + pub fn total_drag_delta_document(&self) -> DVec2 { + self.current_document - self.start_document + } + + pub fn total_drag_delta_viewport(&self, metadata: &DocumentMetadata) -> DVec2 { + metadata.document_to_viewport.transform_vector2(self.total_drag_delta_document()) + } + + pub fn inverse_drag_delta_viewport(&self, metadata: &DocumentMetadata) -> DVec2 { + -self.total_drag_delta_viewport(metadata) + } + + pub fn update_selection_mode(&mut self, metadata: &DocumentMetadata, preferences: &PreferencesMessageHandler) -> SelectionMode { + if let Some(resolved_selection_mode) = self.resolved_selection_mode { + return resolved_selection_mode; + } + if preferences.get_selection_mode() != SelectionMode::Directional { + self.resolved_selection_mode = Some(preferences.get_selection_mode()); + return preferences.get_selection_mode(); + } + + let [start, current] = self.start_current_viewport(metadata); + + // Drag direction cannot be resolved TODO: why not consider only X distance? + if start.distance_squared(current) >= DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2) { + let selection_mode = if current.x < start.x { SelectionMode::Touched } else { SelectionMode::Enclosed }; + self.resolved_selection_mode = Some(selection_mode); + return selection_mode; + } + + SelectionMode::default() + } + + /// A viewport quad representing the drag bounds. Expanded if the start == end + pub fn expanded_selection_box_viewport(&self, metadata: &DocumentMetadata) -> [DVec2; 2] { + let [start, current] = self.start_current_viewport(metadata); + if start == current { + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + [current - tolerance, current + tolerance] + } else { + [start, current] + } + } +} diff --git a/editor/src/messages/tool/tool_messages/select_tool/duplicate.rs b/editor/src/messages/tool/tool_messages/select_tool/duplicate.rs new file mode 100644 index 0000000000..f6b89af80f --- /dev/null +++ b/editor/src/messages/tool/tool_messages/select_tool/duplicate.rs @@ -0,0 +1,105 @@ +use super::super::tool_prelude::*; +use super::drag_state::DragState; +use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeTemplate}; +use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; +use crate::messages::tool::tool_messages::select_tool::LayerNodeIdentifier; +use crate::messages::tool::tool_messages::select_tool::TransformIn; +use graphene_std::uuid::NodeId; + +#[derive(Clone, Debug, Default)] +pub struct DuplcateState { + pub non_duplicated_layers: Option>, +} + +impl DuplcateState { + pub fn reset(&mut self) { + self.non_duplicated_layers = None; + } + + pub fn set_duplicating(&mut self, state: bool, layers_dragging: &mut Vec, document: &mut DocumentMessageHandler, dragging: &DragState, responses: &mut VecDeque) { + if !state && self.non_duplicated_layers.is_some() { + self.stop_duplicates(layers_dragging, document, dragging, responses); + } else if state && self.non_duplicated_layers.is_none() { + self.start_duplicates(layers_dragging, document, dragging, responses); + } + } + + /// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated. + fn start_duplicates(&mut self, layers_dragging: &mut Vec, document: &mut DocumentMessageHandler, dragging: &DragState, responses: &mut VecDeque) { + let mut new_dragging = Vec::new(); + + // Get the shallowest unique layers and sort by their index relative to parent for ordered processing + let layers = document.network_interface.shallowest_unique_layers(&[]).collect::>(); + + for layer in layers.into_iter().rev() { + let Some(parent) = layer.parent(document.metadata()) else { continue }; + + // Moves the layer back to its starting position. + responses.add(GraphOperationMessage::TransformChange { + layer, + transform: DAffine2::from_translation(dragging.inverse_drag_delta_viewport(document.metadata())), + transform_in: TransformIn::Viewport, + skip_rerender: true, + }); + + // Copy the layer + let mut copy_ids = HashMap::new(); + let node_id = layer.to_node(); + copy_ids.insert(node_id, NodeId(0)); + + document + .network_interface + .upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::LayerChildrenUpstreamFlow) + .enumerate() + .for_each(|(index, node_id)| { + copy_ids.insert(node_id, NodeId((index + 1) as u64)); + }); + + let nodes = document.network_interface.copy_nodes(©_ids, &[]).collect::>(); + + let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent); + + let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); + + let layer_id = *new_ids.get(&NodeId(0)).expect("Node Id 0 should be a layer"); + let layer = LayerNodeIdentifier::new_unchecked(layer_id); + new_dragging.push(layer); + responses.add(NodeGraphMessage::AddNodes { nodes, new_ids }); + responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index }); + } + let nodes = new_dragging.iter().filter(|&&layer| layer != LayerNodeIdentifier::ROOT_PARENT).map(|layer| layer.to_node()).collect(); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); + responses.add(NodeGraphMessage::RunDocumentGraph); + self.non_duplicated_layers = Some(core::mem::replace(layers_dragging, new_dragging)); + } + + /// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated. + fn stop_duplicates(&mut self, layers_dragging: &mut Vec, document: &DocumentMessageHandler, dragging: &DragState, responses: &mut VecDeque) { + let Some(original) = self.non_duplicated_layers.take() else { + return; + }; + + // Delete the duplicated layers + for layer in document.network_interface.shallowest_unique_layers(&[]) { + responses.add(NodeGraphMessage::DeleteNodes { + node_ids: vec![layer.to_node()], + delete_children: true, + }); + } + + for &layer in &original { + responses.add(GraphOperationMessage::TransformChange { + layer, + transform: DAffine2::from_translation(dragging.total_drag_delta_viewport(document.metadata())), + transform_in: TransformIn::Viewport, + skip_rerender: true, + }); + } + let nodes = original.iter().filter(|&&layer| layer != LayerNodeIdentifier::ROOT_PARENT).map(|layer| layer.to_node()).collect(); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::SelectedNodesUpdated); + responses.add(NodeGraphMessage::SendGraph); + *layers_dragging = original; + } +} diff --git a/editor/src/messages/tool/tool_messages/select_tool/options.rs b/editor/src/messages/tool/tool_messages/select_tool/options.rs new file mode 100644 index 0000000000..b2c3c0e8be --- /dev/null +++ b/editor/src/messages/tool/tool_messages/select_tool/options.rs @@ -0,0 +1,214 @@ +use super::super::tool_prelude::*; +use super::SelectTool; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; +use crate::messages::tool::common_functionality::pivot::{PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget}; +use graphene_std::path_bool::BooleanOperation; +use graphene_std::vector::ReferencePoint; +use std::fmt; + +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum SelectOptionsUpdate { + NestedSelectionBehavior(NestedSelectionBehavior), + PivotGizmoType(PivotGizmoType), + TogglePivotGizmoType(bool), + TogglePivotPinned, +} + +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum NestedSelectionBehavior { + #[default] + Shallowest, + Deepest, +} + +impl fmt::Display for NestedSelectionBehavior { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NestedSelectionBehavior::Deepest => write!(f, "Deep Select"), + NestedSelectionBehavior::Shallowest => write!(f, "Shallow Select"), + } + } +} + +impl SelectTool { + fn deep_selection_widget(&self) -> WidgetHolder { + let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest] + .iter() + .map(|mode| { + MenuListEntry::new(format!("{mode:?}")) + .label(mode.to_string()) + .on_commit(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into()) + }) + .collect(); + + DropdownInput::new(vec![layer_selection_behavior_entries]) + .selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest) as u32)) + .tooltip( + "Selection Mode\n\ + \n\ + Shallow Select: clicks initially select the least-nested layers and double clicks drill deeper into the folder hierarchy.\n\ + Deep Select: clicks directly select the most-nested layers in the folder hierarchy.", + ) + .widget_holder() + } + + fn alignment_widgets(&self, disabled: bool) -> impl Iterator + use<> { + [AlignAxis::X, AlignAxis::Y] + .into_iter() + .flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)]) + .map(move |(axis, aggregate)| { + let (icon, tooltip) = match (axis, aggregate) { + (AlignAxis::X, AlignAggregate::Min) => ("AlignLeft", "Align Left"), + (AlignAxis::X, AlignAggregate::Center) => ("AlignHorizontalCenter", "Align Horizontal Center"), + (AlignAxis::X, AlignAggregate::Max) => ("AlignRight", "Align Right"), + (AlignAxis::Y, AlignAggregate::Min) => ("AlignTop", "Align Top"), + (AlignAxis::Y, AlignAggregate::Center) => ("AlignVerticalCenter", "Align Vertical Center"), + (AlignAxis::Y, AlignAggregate::Max) => ("AlignBottom", "Align Bottom"), + }; + IconButton::new(icon, 24) + .tooltip(tooltip) + .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()) + .disabled(disabled) + .widget_holder() + }) + } + + fn flip_widgets(&self, disabled: bool) -> impl Iterator + use<> { + [(FlipAxis::X, "Horizontal"), (FlipAxis::Y, "Vertical")].into_iter().map(move |(flip_axis, name)| { + IconButton::new("Flip".to_string() + name, 24) + .tooltip("Flip ".to_string() + name) + .on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into()) + .disabled(disabled) + .widget_holder() + }) + } + + fn turn_widgets(&self, disabled: bool) -> impl Iterator + use<> { + [(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")] + .into_iter() + .map(move |(degrees, icon, name)| { + IconButton::new(icon, 24) + .tooltip(name) + .on_update(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into()) + .disabled(disabled) + .widget_holder() + }) + } + + fn boolean_widgets(&self, selected_count: usize) -> impl Iterator + use<> { + let list = ::list(); + list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| { + let mut tooltip = info.label.to_string(); + if let Some(doc) = info.docstring.as_deref() { + tooltip.push_str("\n\n"); + tooltip.push_str(doc); + } + IconButton::new(info.icon.as_deref().unwrap(), 24) + .tooltip(tooltip) + .disabled(selected_count == 0) + .on_update(move |_| { + let group_folder_type = GroupFolderType::BooleanOperation(*operation); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) + .widget_holder() + }) + } + + pub fn update_tool_options(&mut self, option_update: &SelectOptionsUpdate, responses: &mut VecDeque) { + match option_update { + SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => { + self.tool_data.nested_selection_behavior = *nested_selection_behavior; + responses.add(ToolMessage::UpdateHints); + } + SelectOptionsUpdate::PivotGizmoType(gizmo_type) => { + if !self.tool_data.pivot_gizmo.state.disabled { + self.tool_data.pivot_gizmo.state.gizmo_type = *gizmo_type; + responses.add(ToolMessage::UpdateHints); + let pivot_gizmo = self.tool_data.pivot_gizmo(); + responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo }); + responses.add(NodeGraphMessage::RunDocumentGraph); + self.tool_data.pivot_changed = true; + } + } + SelectOptionsUpdate::TogglePivotGizmoType(state) => { + self.tool_data.pivot_gizmo.state.disabled = !state; + responses.add(ToolMessage::UpdateHints); + responses.add(NodeGraphMessage::RunDocumentGraph); + self.tool_data.pivot_changed = true; + } + + SelectOptionsUpdate::TogglePivotPinned => { + self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned; + responses.add(ToolMessage::UpdateHints); + responses.add(NodeGraphMessage::RunDocumentGraph); + self.tool_data.pivot_changed = true; + } + } + } +} + +impl LayoutHolder for SelectTool { + fn layout(&self) -> Layout { + let mut widgets = Vec::new(); + + // Select mode (Deep/Shallow) + widgets.push(self.deep_selection_widget()); + + // Pivot gizmo type (checkbox + dropdown for pivot/origin) + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.extend(pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Select)); + + if self.tool_data.pivot_gizmo.state.is_pivot_type() { + // Nine-position reference point widget + widgets.push(Separator::new(SeparatorType::Related).widget_holder()); + widgets.push(pivot_reference_point_widget( + self.tool_data.selected_layers_count == 0 || !self.tool_data.pivot_gizmo.state.is_pivot(), + self.tool_data.pivot_gizmo.pivot.to_pivot_position(), + PivotToolSource::Select, + )); + + // Pivot pin button + widgets.push(Separator::new(SeparatorType::Related).widget_holder()); + + let pin_active = self.tool_data.pivot_gizmo.pin_active(); + let pin_enabled = self.tool_data.pivot_gizmo.pivot.old_pivot_position == ReferencePoint::None && !self.tool_data.pivot_gizmo.state.disabled; + + if pin_active || pin_enabled { + widgets.push(pin_pivot_widget(pin_active, pin_enabled, PivotToolSource::Select)); + } + } + + // Align + let disabled = self.tool_data.selected_layers_count < 2; + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.extend(self.alignment_widgets(disabled)); + // widgets.push( + // PopoverButton::new() + // .popover_layout(vec![ + // LayoutGroup::Row { + // widgets: vec![TextLabel::new("Align").bold(true).widget_holder()], + // }, + // LayoutGroup::Row { + // widgets: vec![TextLabel::new("Coming soon").widget_holder()], + // }, + // ]) + // .disabled(disabled) + // .widget_holder(), + // ); + + // Flip + let disabled = self.tool_data.selected_layers_count == 0; + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.extend(self.flip_widgets(disabled)); + + // Turn + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.extend(self.turn_widgets(disabled)); + + // Boolean + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.extend(self.boolean_widgets(self.tool_data.selected_layers_count)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index c6e6911a60..4da06b0e75 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -233,7 +233,7 @@ impl<'a> MessageHandler> for Shap } } - update_dynamic_hints(&self.fsm_state, responses, &self.tool_data); + self.fsm_state.update_hints(&self.tool_data, context, &self.options, responses); self.send_layout(responses, LayoutTarget::ToolOptions); } @@ -914,100 +914,96 @@ impl Fsm for ShapeToolFsmState { } } - fn update_hints(&self, _responses: &mut VecDeque) { - // Moved logic to update_dynamic_hints - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } -} - -fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque, tool_data: &ShapeToolData) { - let hint_data = match state { - ShapeToolFsmState::Ready(_) => { - let hint_groups = match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star => vec![ - HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), - HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + fn update_hints(&self, tool_data: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { + let hint_data = match self { + ShapeToolFsmState::Ready(_) => { + let hint_groups = match tool_data.current_shape { + ShapeType::Polygon | ShapeType::Star => vec![ + HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), + HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ]), + HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), + ], + ShapeType::Ellipse => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), + HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Line => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), + HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), + ])], + ShapeType::Rectangle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), + HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Circle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"), HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Arc => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"), + HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + }; + HintData(hint_groups) + } + ShapeToolFsmState::Drawing(shape) => { + let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; + let tool_hint_group = match shape { + ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Line => HintGroup(vec![ + HintInfo::keys([Key::Shift], "15° Increments"), + HintInfo::keys([Key::Alt], "From Center"), + HintInfo::keys([Key::Control], "Lock Angle"), ]), - HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), - ], - ShapeType::Ellipse => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), - HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - ShapeType::Line => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), - HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), - ])], - ShapeType::Rectangle => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), - HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - ShapeType::Circle => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - ShapeType::Arc => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"), - HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - }; - HintData(hint_groups) - } - ShapeToolFsmState::Drawing(shape) => { - let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; - let tool_hint_group = match shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Line => HintGroup(vec![ + ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), + }; + + if !tool_hint_group.0.is_empty() { + common_hint_group.push(tool_hint_group); + } + + if matches!(shape, ShapeType::Polygon | ShapeType::Star) { + common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); + } + + HintData(common_hint_group) + } + ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![ HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Alt], "From Center"), HintInfo::keys([Key::Control], "Lock Angle"), ]), - ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), - }; - - if !tool_hint_group.0.is_empty() { - common_hint_group.push(tool_hint_group); - } - - if matches!(shape, ShapeType::Polygon | ShapeType::Star) { - common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); - } - - HintData(common_hint_group) - } - ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![ - HintInfo::keys([Key::Shift], "15° Increments"), - HintInfo::keys([Key::Alt], "From Center"), - HintInfo::keys([Key::Control], "Lock Angle"), ]), - ]), - ShapeToolFsmState::ResizingBounds => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]), - ]), - ShapeToolFsmState::RotatingBounds => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), - ]), - ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), - ]), - ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), - }; - responses.add(FrontendMessage::UpdateInputHints { hint_data }); + ShapeToolFsmState::ResizingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]), + ]), + ShapeToolFsmState::RotatingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), + ]), + ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), + ]), + ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), + }; + responses.add(FrontendMessage::UpdateInputHints { hint_data }); + } + + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); + } } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 6e455b2939..7944b30c3c 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -450,7 +450,7 @@ impl Fsm for SplineToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::Lmb, "Draw Spline"), @@ -467,7 +467,7 @@ impl Fsm for SplineToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 1e2cc3f6ab..57650c0867 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -393,12 +393,24 @@ impl TextToolData { Fill::None }, }); - responses.add(GraphOperationMessage::TransformSet { - layer: self.layer, - transform: editing_text.transform, - transform_in: TransformIn::Viewport, - skip_rerender: true, + + responses.add(DeferMessage::AfterGraphRun { + messages: vec![ + GraphOperationMessage::TransformSet { + layer: self.layer, + transform: editing_text.transform, + transform_in: TransformIn::Viewport, + skip_rerender: true, + } + .into(), + DeferMessage::AfterGraphRun { + messages: vec![OverlaysMessage::Draw.into()], + } + .into(), + NodeGraphMessage::RunDocumentGraph.into(), + ], }); + self.editing_text = Some(editing_text); self.set_editing(true, font_cache, responses); @@ -475,9 +487,9 @@ impl Fsm for TextToolFsmState { let ToolMessage::Text(event) = event else { return self }; match (self, event) { (TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => { - responses.add(FrontendMessage::DisplayEditableTextboxTransform { - transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(), - }); + let t = document.metadata().transform_to_viewport(tool_data.layer); + warn!("Set transform {t}"); + responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform: t.to_cols_array() }); if let Some(editing_text) = tool_data.editing_text.as_mut() { let font_data = font_cache.get(&editing_text.font).map(|data| load_font(data)); let far = graphene_std::text::bounding_box(&tool_data.new_text, font_data, editing_text.typesetting, false); @@ -886,7 +898,7 @@ impl Fsm for TextToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { + fn update_hints(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let hint_data = match self { TextToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Place Text")]), @@ -915,7 +927,7 @@ impl Fsm for TextToolFsmState { responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - fn update_cursor(&self, responses: &mut VecDeque) { + fn update_cursor(&self, _: &Self::ToolData, _: &ToolActionMessageContext, _: &Self::ToolOptions, responses: &mut VecDeque) { let cursor = match self { TextToolFsmState::Placing => MouseCursorIcon::Crosshair, _ => MouseCursorIcon::Text, diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 740b09c972..041747f062 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -56,20 +56,27 @@ pub trait Fsm { fn transition(self, message: ToolMessage, tool_data: &mut Self::ToolData, transition_data: &mut ToolActionMessageContext, options: &Self::ToolOptions, responses: &mut VecDeque) -> Self; /// Implementing this trait function lets a specific tool provide a list of hints (user input actions presently available) to draw in the footer bar. - fn update_hints(&self, responses: &mut VecDeque); + fn update_hints(&self, tool_data: &Self::ToolData, transition_data: &ToolActionMessageContext, options: &Self::ToolOptions, responses: &mut VecDeque); /// Implementing this trait function lets a specific tool set the current mouse cursor icon. - fn update_cursor(&self, responses: &mut VecDeque); + fn update_cursor(&self, tool_data: &Self::ToolData, transition_data: &ToolActionMessageContext, options: &Self::ToolOptions, responses: &mut VecDeque); /// If this message is a standard tool message, process it and return true. Standard tool messages are those which are common across every tool. - fn standard_tool_messages(&self, message: &ToolMessage, responses: &mut VecDeque) -> bool { + fn standard_tool_messages( + &self, + message: &ToolMessage, + tool_data: &Self::ToolData, + transition_data: &ToolActionMessageContext, + options: &Self::ToolOptions, + responses: &mut VecDeque, + ) -> bool { // Check for standard hits or cursor events match message { ToolMessage::UpdateHints => { - self.update_hints(responses); + self.update_hints(tool_data, transition_data, options, responses); true } ToolMessage::UpdateCursor => { - self.update_cursor(responses); + self.update_cursor(tool_data, transition_data, options, responses); true } _ => false, @@ -90,7 +97,7 @@ pub trait Fsm { Self: PartialEq + Sized + Copy, { // If this message is one of the standard tool messages, process it and exit early - if self.standard_tool_messages(&message, responses) { + if self.standard_tool_messages(&message, tool_data, transition_data, options, responses) { return; } @@ -100,9 +107,9 @@ pub trait Fsm { // Update state if *self != new_state { *self = new_state; - self.update_hints(responses); + self.update_hints(tool_data, transition_data, options, responses); if update_cursor_on_transition { - self.update_cursor(responses); + self.update_cursor(tool_data, transition_data, options, responses); } } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c395ae0de6..d8405fcb4f 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -83,6 +83,13 @@ impl NodeGraphExecutor { }; (node_runtime, node_executor) } + + /// It is useful to get the current execution id for tests to check if any more exeuctions have been queued. + #[cfg(test)] + pub(crate) fn current_execution_id(&self) -> u64 { + self.current_execution_id + } + /// Execute the network by flattening it and creating a borrow stack. fn queue_execution(&mut self, render_config: RenderConfig) -> u64 { let execution_id = self.current_execution_id; @@ -290,7 +297,7 @@ impl NodeGraphExecutor { } else { self.process_node_graph_output(node_graph_output, transform, responses)? } - responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id)); + responses.add(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id)); // Update the spreadsheet on the frontend using the value of the inspect result. if self.old_inspect_node.is_some() { diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index addadae0c2..1f469aa3e2 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -41,7 +41,8 @@ impl EditorTestUtils { async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result { let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; let exector = &mut portfolio.executor; - let document = portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap(); + let document_id = portfolio.active_document_id.unwrap(); + let document = portfolio.documents.get_mut(&document_id).unwrap(); let instrumented = match exector.update_node_graph_instrumented(document) { Ok(instrumented) => instrumented, @@ -49,19 +50,29 @@ impl EditorTestUtils { }; let viewport_resolution = glam::UVec2::ONE; - if let Err(e) = exector.submit_current_node_graph_evaluation(document, DocumentId(0), viewport_resolution, Default::default()) { + if let Err(e) = exector.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, Default::default()) { return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}")); } - runtime.run().await; - let mut messages = VecDeque::new(); - if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) { - return Err(format!("Graph should render\n\n{e}")); - } - let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message)); + // Run until no more executions are queued. + loop { + println!("Running graph"); + let execution_id = editor.dispatcher.message_handlers.portfolio_message_handler.executor.current_execution_id(); + runtime.run().await; + + let mut messages = VecDeque::new(); + if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) { + return Err(format!("Graph should render\n\n{e}")); + } + let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message)); - for message in frontend_messages { - message.check_node_graph_error(); + for message in frontend_messages { + message.check_node_graph_error(); + } + let next_execution_id = editor.dispatcher.message_handlers.portfolio_message_handler.executor.current_execution_id(); + if next_execution_id == execution_id { + break; + } } Ok(instrumented) @@ -258,12 +269,24 @@ impl EditorTestUtils { self.active_document().network_interface.selected_nodes().selected_layers(self.active_document().metadata()).next() } - pub async fn double_click(&mut self, position: DVec2) { + /// Simulate a pointer up then a double click without seperation of a graph render. + /// + /// This seems to be what WASM does to the test. + pub async fn pointer_up_double_click(&mut self, editor_position: DVec2) { + self.left_mousedown(editor_position.x, editor_position.y, ModifierKeys::empty()).await; + // Simulate a mouse up then double click event without a rerender then double click + self.editor.handle_message(InputPreprocessorMessage::PointerUp { + editor_mouse_state: EditorMouseState { + editor_position, + ..Default::default() + }, + modifier_keys: ModifierKeys::empty(), + }); self.handle_message(InputPreprocessorMessage::DoubleClick { editor_mouse_state: EditorMouseState { - editor_position: position, + editor_position, mouse_keys: MouseKeys::LEFT, - scroll_delta: ScrollDelta::default(), + ..Default::default() }, modifier_keys: ModifierKeys::empty(), })