Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Selection direction difference
The selection direction should make a difference.
Usually in apps like these if you start a selection frame by dragging the mouse to the right it should only select objects that are fully inside of the selection frame. If the user drags to the left everything that is _touched_ by the selection frame should be selected.


Discussion / hints:

Fix patch:
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<Item = LayerNodeIdentifier> + '_ {
 		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<LayerNodeIdentifier>,
+	pub parent: Option<LayerNodeIdentifier>,
 	previous_sibling: Option<LayerNodeIdentifier>,
 	next_sibling: Option<LayerNodeIdentifier>,
 	first_child: Option<LayerNodeIdentifier>,
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<u8>,
 	},
-	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<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
 	pub persistent_data: PersistentData,
 	pub executor: NodeGraphExecutor,
+	pub selection_mode: SelectionMode,
 }
 
 impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
@@ -295,35 +297,35 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> 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<PreferencesMessage, ()> for PreferencesMessageHandler {
 	fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _data: ()) {
 		match message {
+			// Management messages
 			PreferencesMessage::Load { preferences } => {
 				if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
 					*self = deserialized_preferences;
@@ -65,31 +74,12 @@ impl MessageHandler<PreferencesMessage, ()> 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<PreferencesMessage, ()> 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)]


Test patch: