diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 22d331e7f2..1dee397075 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -221,20 +221,21 @@ impl Dispatcher { } Message::Tool(message) => { let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap(); - if let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) { - let data = ToolMessageData { - document_id, - document, - input: &self.message_handlers.input_preprocessor_message_handler, - persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data, - node_graph: &self.message_handlers.portfolio_message_handler.executor, - preferences: &self.message_handlers.preferences_message_handler, - }; - - self.message_handlers.tool_message_handler.process_message(message, &mut queue, data); - } else { + let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else { warn!("Called ToolMessage without an active document.\nGot {message:?}"); - } + return; + }; + + let data = ToolMessageData { + document_id, + document, + input: &self.message_handlers.input_preprocessor_message_handler, + persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data, + node_graph: &self.message_handlers.portfolio_message_handler.executor, + preferences: &self.message_handlers.preferences_message_handler, + }; + + self.message_handlers.tool_message_handler.process_message(message, &mut queue, data); } Message::Workspace(message) => { self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ()); diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index b4375a7f01..a4b13e70fb 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,4 +1,5 @@ use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; pub struct PreferencesDialogMessageData<'a> { @@ -31,6 +32,39 @@ impl PreferencesDialogMessageHandler { const TITLE: &'static str = "Editor Preferences"; fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { + let selection_section = vec![TextLabel::new("Selection").italic(true).widget_holder()]; + let selection_mode = RadioInput::new(vec![ + RadioEntryData::new(SelectionMode::Touched.to_string()) + .label(SelectionMode::Touched.to_string()) + .tooltip(SelectionMode::Touched.tooltip_description()) + .on_update(move |_| { + PreferencesMessage::SelectionMode { + selection_mode: SelectionMode::Touched, + } + .into() + }), + RadioEntryData::new(SelectionMode::Enclosed.to_string()) + .label(SelectionMode::Enclosed.to_string()) + .tooltip(SelectionMode::Enclosed.tooltip_description()) + .on_update(move |_| { + PreferencesMessage::SelectionMode { + selection_mode: SelectionMode::Enclosed, + } + .into() + }), + RadioEntryData::new(SelectionMode::Directional.to_string()) + .label(SelectionMode::Directional.to_string()) + .tooltip(SelectionMode::Directional.tooltip_description()) + .on_update(move |_| { + PreferencesMessage::SelectionMode { + selection_mode: SelectionMode::Directional, + } + .into() + }), + ]) + .selected_index(Some(preferences.selection_mode as u32)) + .widget_holder(); + let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)"; let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()]; let zoom_with_scroll = vec![ @@ -43,9 +77,9 @@ impl PreferencesDialogMessageHandler { .into() }) .widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(), ]; + let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)"; let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()]; let use_vello = vec![ @@ -54,7 +88,6 @@ impl PreferencesDialogMessageHandler { .disabled(!preferences.supports_wgpu()) .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into()) .widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), TextLabel::new("Vello Renderer") .table_align(true) .tooltip(vello_tooltip) @@ -62,11 +95,19 @@ impl PreferencesDialogMessageHandler { .widget_holder(), ]; + let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills."; + let vector_meshes = vec![ + CheckboxInput::new(preferences.vector_meshes) + .tooltip(vector_mesh_tooltip) + .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into()) + .widget_holder(), + TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(), + ]; + // TODO: Reenable when Imaginate is restored // let imaginate_server_hostname = vec![ // TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(), // TextLabel::new("Server Hostname").table_align(true).widget_holder(), - // Separator::new(SeparatorType::Unrelated).widget_holder(), // TextInput::new(&preferences.imaginate_server_hostname) // .min_width(200) // .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into()) @@ -75,7 +116,6 @@ impl PreferencesDialogMessageHandler { // let imaginate_refresh_frequency = vec![ // TextLabel::new("").min_width(60).widget_holder(), // TextLabel::new("Refresh Frequency").table_align(true).widget_holder(), - // Separator::new(SeparatorType::Unrelated).widget_holder(), // NumberInput::new(Some(preferences.imaginate_refresh_frequency)) // .unit(" seconds") // .min(0.) @@ -85,17 +125,9 @@ impl PreferencesDialogMessageHandler { // .widget_holder(), // ]; - let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills."; - let vector_meshes = vec![ - CheckboxInput::new(preferences.vector_meshes) - .tooltip(vector_mesh_tooltip) - .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into()) - .widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), - TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(), - ]; - Layout::WidgetLayout(WidgetLayout::new(vec![ + LayoutGroup::Row { widgets: selection_section }, + LayoutGroup::Row { widgets: vec![selection_mode] }, LayoutGroup::Row { widgets: input_section }, LayoutGroup::Row { widgets: zoom_with_scroll }, LayoutGroup::Row { widgets: renderer_section }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 0c45330ba1..fefed2da90 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1407,6 +1407,29 @@ impl DocumentMessageHandler { self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) } + pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_core::renderer::Quad) -> bool { + // Get the bounding box of the layer in document space + let Some(bounding_box) = self.metadata().bounding_box_viewport(*layer) else { return false }; + + // Check if the bounding box is fully within the selection quad + let [top_left, bottom_right] = bounding_box; + + let quad_bbox = quad.bounding_box(); + + let quad_left = quad_bbox[0].x; + let quad_right = quad_bbox[1].x; + let quad_top = quad_bbox[0].y.max(quad_bbox[1].y); // Correct top + let quad_bottom = quad_bbox[0].y.min(quad_bbox[1].y); // Correct bottom + + // Extract layer's bounding box coordinates + let layer_left = top_left.x; + let layer_right = bottom_right.x; + let layer_top = bottom_right.y; + let layer_bottom = top_left.y; + + layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom + } + /// Find all of the layers that were clicked on from a viewport space location pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + '_ { let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); 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 e4e8309182..120e3e3297 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -460,7 +460,7 @@ impl DoubleEndedIterator for DescendantsIter<'_> { #[derive(Debug, Clone, Copy, Default)] pub struct NodeRelations { - parent: Option, + pub parent: Option, previous_sibling: Option, next_sibling: Option, first_child: Option, diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 6fe3df5964..f60f98d7c1 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -46,16 +46,16 @@ pub enum PortfolioMessage { document_id: DocumentId, }, DestroyAllDocuments, + EditorPreferences, FontLoaded { font_family: String, font_style: String, preview_url: String, data: Vec, }, - ImaginateCheckServerStatus, - ImaginatePollServerStatus, - EditorPreferences, - ImaginateServerHostname, + // ImaginateCheckServerStatus, + // ImaginatePollServerStatus, + // ImaginateServerHostname, Import, LoadDocumentResources { document_id: DocumentId, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index a2a3e2d5f6..9500656577 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -9,6 +9,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; 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::DocumentMessageData; +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; @@ -38,6 +39,7 @@ pub struct PortfolioMessageHandler { copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], pub persistent_data: PersistentData, pub executor: NodeGraphExecutor, + pub selection_mode: SelectionMode, } impl MessageHandler> for PortfolioMessageHandler { @@ -295,35 +297,35 @@ impl MessageHandler> for PortfolioMes responses.add(NodeGraphMessage::RunDocumentGraph); } } - PortfolioMessage::ImaginateCheckServerStatus => { - let server_status = self.persistent_data.imaginate.server_status().clone(); - self.persistent_data.imaginate.poll_server_check(); - #[cfg(target_arch = "wasm32")] - if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() { - wasm_bindgen_futures::spawn_local(async move { - let () = fut.await; - use wasm_bindgen::prelude::*; - - #[wasm_bindgen(module = "/../frontend/src/editor.ts")] - extern "C" { - #[wasm_bindgen(js_name = injectImaginatePollServerStatus)] - fn inject(); - } - inject(); - }) - } - if &server_status != self.persistent_data.imaginate.server_status() { - responses.add(PropertiesPanelMessage::Refresh); - } - } - PortfolioMessage::ImaginatePollServerStatus => { - self.persistent_data.imaginate.poll_server_check(); - responses.add(PropertiesPanelMessage::Refresh); - } + // PortfolioMessage::ImaginateCheckServerStatus => { + // let server_status = self.persistent_data.imaginate.server_status().clone(); + // self.persistent_data.imaginate.poll_server_check(); + // #[cfg(target_arch = "wasm32")] + // if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() { + // wasm_bindgen_futures::spawn_local(async move { + // let () = fut.await; + // use wasm_bindgen::prelude::*; + + // #[wasm_bindgen(module = "/../frontend/src/editor.ts")] + // extern "C" { + // #[wasm_bindgen(js_name = injectImaginatePollServerStatus)] + // fn inject(); + // } + // inject(); + // }) + // } + // if &server_status != self.persistent_data.imaginate.server_status() { + // responses.add(PropertiesPanelMessage::Refresh); + // } + // } + // PortfolioMessage::ImaginatePollServerStatus => { + // self.persistent_data.imaginate.poll_server_check(); + // responses.add(PropertiesPanelMessage::Refresh); + // } PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), - PortfolioMessage::ImaginateServerHostname => { - self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname); - } + // PortfolioMessage::ImaginateServerHostname => { + // self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname); + // } PortfolioMessage::Import => { // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages responses.add(FrontendMessage::TriggerImport); diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index a44e47838d..73203dbaca 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,4 +1,5 @@ -use graphene_std::{imaginate::ImaginatePersistentData, text::FontCache}; +use graphene_std::imaginate::ImaginatePersistentData; +use graphene_std::text::FontCache; #[derive(Debug, Default)] pub struct PersistentData { diff --git a/editor/src/messages/preferences/mod.rs b/editor/src/messages/preferences/mod.rs index f61ee402ce..916c384f8a 100644 --- a/editor/src/messages/preferences/mod.rs +++ b/editor/src/messages/preferences/mod.rs @@ -1,7 +1,10 @@ mod preferences_message; mod preferences_message_handler; +pub mod utility_types; #[doc(inline)] pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant}; #[doc(inline)] pub use preferences_message_handler::PreferencesMessageHandler; +#[doc(inline)] +pub use utility_types::SelectionMode; diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index 692b21e23b..d060267a70 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -1,14 +1,18 @@ +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; #[impl_message(Message, Preferences)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PreferencesMessage { + // Management messages Load { preferences: String }, ResetToDefaults, - ImaginateRefreshFrequency { seconds: f64 }, + // Per-preference messages UseVello { use_vello: bool }, - ImaginateServerHostname { hostname: String }, + SelectionMode { selection_mode: SelectionMode }, VectorMeshes { enabled: bool }, ModifyLayout { zoom_with_scroll: bool }, + // ImaginateRefreshFrequency { seconds: f64 }, + // ImaginateServerHostname { hostname: String }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 55b4323670..f52b109b9a 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -1,17 +1,24 @@ use crate::messages::input_mapper::key_mapping::MappingVariant; +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; + use graph_craft::wasm_application_io::EditorPreferences; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)] pub struct PreferencesMessageHandler { pub imaginate_server_hostname: String, pub imaginate_refresh_frequency: f64, + pub selection_mode: SelectionMode, pub zoom_with_scroll: bool, pub use_vello: bool, pub vector_meshes: bool, } impl PreferencesMessageHandler { + pub fn get_selection_mode(&self) -> SelectionMode { + self.selection_mode + } + pub fn editor_preferences(&self) -> EditorPreferences { EditorPreferences { imaginate_hostname: self.imaginate_server_hostname.clone(), @@ -33,6 +40,7 @@ impl Default for PreferencesMessageHandler { Self { imaginate_server_hostname: host_name, imaginate_refresh_frequency: 1., + selection_mode: SelectionMode::Touched, zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), use_vello, vector_meshes: false, @@ -43,6 +51,7 @@ impl Default for PreferencesMessageHandler { impl MessageHandler for PreferencesMessageHandler { fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque, _data: ()) { match message { + // Management messages PreferencesMessage::Load { preferences } => { if let Ok(deserialized_preferences) = serde_json::from_str::(&preferences) { *self = deserialized_preferences; @@ -65,31 +74,12 @@ impl MessageHandler for PreferencesMessageHandler { *self = Self::default() } - PreferencesMessage::ImaginateRefreshFrequency { seconds } => { - self.imaginate_refresh_frequency = seconds; - responses.add(PortfolioMessage::ImaginateCheckServerStatus); - responses.add(PortfolioMessage::EditorPreferences); - } + // Per-preference messages PreferencesMessage::UseVello { use_vello } => { self.use_vello = use_vello; responses.add(PortfolioMessage::UpdateVelloPreference); responses.add(PortfolioMessage::EditorPreferences); } - PreferencesMessage::ImaginateServerHostname { hostname } => { - let initial = hostname.clone(); - let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://"); - let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname }; - let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" }; - - if hostname != initial { - refresh_dialog(responses); - } - - self.imaginate_server_hostname = hostname; - responses.add(PortfolioMessage::ImaginateServerHostname); - responses.add(PortfolioMessage::ImaginateCheckServerStatus); - responses.add(PortfolioMessage::EditorPreferences); - } PreferencesMessage::VectorMeshes { enabled } => { self.vector_meshes = enabled; } @@ -102,7 +92,31 @@ impl MessageHandler for PreferencesMessageHandler { }; responses.add(KeyMappingMessage::ModifyMapping(variant)); } + PreferencesMessage::SelectionMode { selection_mode } => { + self.selection_mode = selection_mode; + } } + // TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block) + // PreferencesMessage::ImaginateRefreshFrequency { seconds } => { + // self.imaginate_refresh_frequency = seconds; + // responses.add(PortfolioMessage::ImaginateCheckServerStatus); + // responses.add(PortfolioMessage::EditorPreferences); + // } + // PreferencesMessage::ImaginateServerHostname { hostname } => { + // let initial = hostname.clone(); + // let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://"); + // let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname }; + // let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" }; + + // if hostname != initial { + // refresh_dialog(responses); + // } + + // self.imaginate_server_hostname = hostname; + // responses.add(PortfolioMessage::ImaginateServerHostname); + // responses.add(PortfolioMessage::ImaginateCheckServerStatus); + // responses.add(PortfolioMessage::EditorPreferences); + //} responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); } diff --git a/editor/src/messages/preferences/utility_types.rs b/editor/src/messages/preferences/utility_types.rs new file mode 100644 index 0000000000..a2ee5dece1 --- /dev/null +++ b/editor/src/messages/preferences/utility_types.rs @@ -0,0 +1,27 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type, Hash)] +pub enum SelectionMode { + #[default] + Touched = 0, + Enclosed = 1, + Directional = 2, +} + +impl std::fmt::Display for SelectionMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SelectionMode::Touched => write!(f, "Touched"), + SelectionMode::Enclosed => write!(f, "Enclosed"), + SelectionMode::Directional => write!(f, "Directional"), + } + } +} + +impl SelectionMode { + pub fn tooltip_description(&self) -> &'static str { + match self { + SelectionMode::Touched => "Select all layers at least partially covered by the dragged selection area", + SelectionMode::Enclosed => "Select only layers fully enclosed by the dragged selection area", + SelectionMode::Directional => r#""Touched" for leftward drags, "Enclosed" for rightward drags"#, + } + } +} diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index e1fcb6dd83..bd12e5dcfa 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -21,11 +21,11 @@ pub struct SizeSnapData<'a> { /// Contains the edges that are being dragged along with the original bounds. #[derive(Clone, Debug, Default)] pub struct SelectedEdges { - bounds: [DVec2; 2], - top: bool, - bottom: bool, - left: bool, - right: bool, + pub bounds: [DVec2; 2], + pub top: bool, + pub bottom: bool, + pub left: bool, + pub right: bool, // Aspect ratio in the form of width/height, so x:1 = width:height aspect_ratio: f64, } diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index ed9e8c23f7..f812f63a22 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -1,4 +1,5 @@ use super::utility_types::ToolType; +use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use graphene_core::raster::color::Color; @@ -98,4 +99,7 @@ pub enum ToolMessage { Undo, UpdateCursor, UpdateHints, + UpdateSelectionMode { + selection_mode: SelectionMode, + }, } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 044b981a58..91e877e9e6 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -9,6 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::transformation::Selected; +use crate::messages::preferences::SelectionMode; use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name}; use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; @@ -250,12 +251,13 @@ impl ToolTransition for SelectTool { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum SelectToolFsmState { Ready { selection: NestedSelectionBehavior }, - DrawingBox { selection: NestedSelectionBehavior }, + DrawingBox, Dragging, ResizingBounds, RotatingBounds, DraggingPivot, } + impl Default for SelectToolFsmState { fn default() -> Self { let selection = NestedSelectionBehavior::Deepest; @@ -297,12 +299,22 @@ impl SelectToolData { } } - fn selection_quad(&self) -> Quad { + pub fn selection_quad(&self) -> Quad { let bbox = self.selection_box(); Quad::from_box(bbox) } - fn selection_box(&self) -> [DVec2; 2] { + pub fn calculate_direction(&self) -> SelectionMode { + let bbox: [DVec2; 2] = self.selection_box(); + 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 + } + } + + 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] @@ -475,21 +487,41 @@ impl Fsm for SelectToolFsmState { tool_data.pivot.update_pivot(document, &mut overlay_context); // Check if the tool is in box selection mode - if matches!(self, Self::DrawingBox { .. }) { + if matches!(self, Self::DrawingBox) { // Get the updated selection box bounds let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]); + let mut selection_direction = tool_action_data.preferences.get_selection_mode(); + if selection_direction == SelectionMode::Directional { + selection_direction = tool_data.calculate_direction(); + } + // Draw outline visualizations on the layers to be selected - for layer in document.intersect_quad_no_artboards(quad, input) { - overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + let mut draw_layer_outline = |layer| overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + let intersection = document.intersect_quad_no_artboards(quad, input); + if selection_direction == SelectionMode::Enclosed { + for layer in intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)) { + draw_layer_outline(layer); + } + } else { + for layer in intersection { + draw_layer_outline(layer); + } } // Update the selection box - let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) .rgba_hex(); - overlay_context.quad(quad, Some(&("#".to_string() + &fill_color))); + fill_color.insert(0, '#'); + let fill_color = Some(fill_color.as_str()); + + if selection_direction == SelectionMode::Enclosed { + overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5)); + } else { + overlay_context.quad(quad, fill_color); + } } // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) // TODO: Don't use `Key::Mmb` directly, instead take it as a variable from the input mappings list like in all other places @@ -675,9 +707,7 @@ impl Fsm for SelectToolFsmState { responses.add(DocumentMessage::StartTransaction); SelectToolFsmState::Dragging } else { - // Make a box selection, preserving previously selected layers - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::DrawingBox { selection } + SelectToolFsmState::DrawingBox } }; tool_data.non_duplicated_layers = None; @@ -824,7 +854,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::DraggingPivot } - (SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerMove(modifier_keys)) => { + (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => { tool_data.drag_current = input.mouse.position; responses.add(OverlaysMessage::Draw); @@ -835,8 +865,7 @@ impl Fsm for SelectToolFsmState { ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - let selection = tool_data.nested_selection_behavior; - SelectToolFsmState::DrawingBox { selection } + SelectToolFsmState::DrawingBox } (SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => { let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); @@ -883,7 +912,7 @@ impl Fsm for SelectToolFsmState { self } - (SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerOutsideViewport(_)) => { + (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(_)) => { // AutoPanning if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { tool_data.drag_start += shift; @@ -1012,14 +1041,26 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Ready { selection } } ( - SelectToolFsmState::DrawingBox { .. }, + SelectToolFsmState::DrawingBox, SelectToolMessage::DragStop { remove_from_selection, negative_box_selection, }, ) => { let quad = tool_data.selection_quad(); - let new_selected: HashSet<_> = document.intersect_quad_no_artboards(quad, input).collect(); + + let mut selection_direction = tool_action_data.preferences.get_selection_mode(); + if selection_direction == SelectionMode::Directional { + selection_direction = tool_data.calculate_direction(); + } + + let intersection = document.intersect_quad_no_artboards(quad, input); + let new_selected: HashSet<_> = if selection_direction == SelectionMode::Enclosed { + intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)).collect() + } else { + intersection.collect() + }; + let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect(); if new_selected != current_selected { // Negative selection when both Shift and Ctrl are pressed @@ -1166,7 +1207,7 @@ impl Fsm for SelectToolFsmState { ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - SelectToolFsmState::DrawingBox { .. } => { + SelectToolFsmState::DrawingBox => { let hint_data = HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![HintInfo::keys([Key::Control, Key::Shift], "Remove from Selection").add_mac_keys([Key::Command, Key::Shift])]), diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index c5f685d495..37d3e21911 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -9,6 +9,7 @@ use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; +use crate::messages::preferences::PreferencesMessageHandler; use crate::messages::prelude::*; use crate::node_graph_executor::NodeGraphExecutor; diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index de7035ccb6..cb3bf494fd 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -81,7 +81,7 @@ export function createEditor(): Editor { // TODO: Then, delete the `(window as any).editorHandle = handle;` line above. // This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system. // Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code. -export function injectImaginatePollServerStatus() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).editorHandle?.injectImaginatePollServerStatus(); -} +// export function injectImaginatePollServerStatus() { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// (window as any).editorHandle?.injectImaginatePollServerStatus(); +// } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 19b7550863..5b14669b8e 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -725,10 +725,10 @@ impl EditorHandle { self.dispatch(message); } - #[wasm_bindgen(js_name = injectImaginatePollServerStatus)] - pub fn inject_imaginate_poll_server_status(&self) { - self.dispatch(PortfolioMessage::ImaginatePollServerStatus); - } + // #[wasm_bindgen(js_name = injectImaginatePollServerStatus)] + // pub fn inject_imaginate_poll_server_status(&self) { + // self.dispatch(PortfolioMessage::ImaginatePollServerStatus); + // } // TODO: Eventually remove this document upgrade code #[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]