From f122517028e120db03d500aec2f75c0acd411f13 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Fri, 3 Apr 2026 23:15:02 +0530 Subject: [PATCH 1/6] Add DocumentMode groundwork with MaskMode and Q toggle --- .../src/messages/input_mapper/input_mappings.rs | 1 + .../portfolio/document/document_message.rs | 6 +++++- .../document/document_message_handler.rs | 17 ++++++++++++++++- .../portfolio/document/utility_types/misc.rs | 17 ++++++++++------- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index b9bb234dc9..ffcb733624 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -339,6 +339,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { // // DocumentMessage entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle), + entry!(KeyDown(KeyQ); action_dispatch=DocumentMessage::ToggleDocumentMode), entry!(KeyDownNoRepeat(Escape); action_dispatch=DocumentMessage::Escape), entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers), entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index dcd528c9e4..4463e441cb 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -6,7 +6,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::data_panel::DataPanelMessage; use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping}; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, GridSnapping}; use crate::messages::portfolio::utility_types::PanelType; use crate::messages::prelude::*; use glam::{DAffine2, IVec2}; @@ -187,6 +187,10 @@ pub enum DocumentMessage { SetRenderMode { render_mode: RenderMode, }, + ToggleDocumentMode, + SetDocumentMode { + document_mode: DocumentMode, + }, AddTransaction, StartTransaction, EndTransaction, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f093a90999..2e218601b1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -18,7 +18,7 @@ use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot}; use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; -use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::utility_types::{PanelType, PersistentData}; use crate::messages::prelude::*; @@ -115,6 +115,9 @@ pub struct DocumentMessageHandler { /// The name of the document, which is displayed in the tab and title bar of the editor. #[serde(skip)] pub name: String, + /// The current editor-only mode for the active document. + #[serde(skip)] + pub document_mode: DocumentMode, /// The path of the to the document file. #[serde(skip)] pub(crate) path: Option, @@ -173,6 +176,7 @@ impl Default for DocumentMessageHandler { // Fields omitted from the saved document format // ============================================= name: DEFAULT_DOCUMENT_NAME.to_string(), + document_mode: DocumentMode::default(), path: None, breadcrumb_network_path: Vec::new(), selection_network_path: Vec::new(), @@ -1108,6 +1112,15 @@ impl MessageHandler> for DocumentMes self.render_mode = render_mode; responses.add_front(NodeGraphMessage::RunDocumentGraph); } + DocumentMessage::ToggleDocumentMode => { + self.document_mode = match self.document_mode { + DocumentMode::MaskMode => DocumentMode::DesignMode, + _ => DocumentMode::MaskMode, + }; + } + DocumentMessage::SetDocumentMode { document_mode } => { + self.document_mode = document_mode; + } DocumentMessage::AddTransaction => { // Reverse order since they are added to the front responses.add_front(DocumentMessage::CommitTransaction); @@ -1463,6 +1476,8 @@ impl MessageHandler> for DocumentMes SaveDocument, SelectAllLayers, SetSnapping, + ToggleDocumentMode, + SetDocumentMode, ToggleGridVisibility, ToggleOverlaysVisibility, ToggleSnapping, diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 7ea2b5dc21..b94895c6c7 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -28,13 +28,15 @@ pub enum AlignAggregate { Center, } -// #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] -// pub enum DocumentMode { -// #[default] -// DesignMode, -// SelectMode, -// GuideMode, -// } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +pub enum DocumentMode { + #[default] + DesignMode, + SelectMode, + GuideMode, + MaskMode, +} // impl fmt::Display for DocumentMode { // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -42,6 +44,7 @@ pub enum AlignAggregate { // DocumentMode::DesignMode => write!(f, "Design Mode"), // DocumentMode::SelectMode => write!(f, "Select Mode"), // DocumentMode::GuideMode => write!(f, "Guide Mode"), +// DocumentMode::MaskMode => write!(f, "Mask Mode"), // } // } // } From 268ec733b4414633d7bd98086d21cc4669ac92d6 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Thu, 9 Apr 2026 14:20:45 +0530 Subject: [PATCH 2/6] add three new DocumentMessage variants --- editor/src/messages/portfolio/document/document_message.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 4463e441cb..e0ce5a71df 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -191,6 +191,13 @@ pub enum DocumentMessage { SetDocumentMode { document_mode: DocumentMode, }, + EnterMaskMode, + ExitMaskMode { + discard: bool, + }, + DrawMarchingAntsOverlay { + context: OverlayContext, + }, AddTransaction, StartTransaction, EndTransaction, From a97d49d8bc1c5f1ffca3ca55db34826ffd1fcf38 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Thu, 9 Apr 2026 16:03:41 +0530 Subject: [PATCH 3/6] fix match error --- .../portfolio/document/document_message_handler.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 2e218601b1..905d6e4d79 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1121,6 +1121,13 @@ impl MessageHandler> for DocumentMes DocumentMessage::SetDocumentMode { document_mode } => { self.document_mode = document_mode; } + DocumentMessage::EnterMaskMode => { + self.document_mode = DocumentMode::MaskMode; + } + DocumentMessage::ExitMaskMode { discard: _ } => { + self.document_mode = DocumentMode::DesignMode; + } + DocumentMessage::DrawMarchingAntsOverlay { context: _ } => {} DocumentMessage::AddTransaction => { // Reverse order since they are added to the front responses.add_front(DocumentMessage::CommitTransaction); From 5b1ca12f536ea9fb9fcb7e0a9ddced21ed3c87bc Mon Sep 17 00:00:00 2001 From: krVatsal Date: Fri, 10 Apr 2026 09:55:39 +0530 Subject: [PATCH 4/6] ui refresh after mode change and activate display for DocumentMode --- .../document/document_message_handler.rs | 16 ++++++++++++--- .../portfolio/document/utility_types/misc.rs | 20 +++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 155df12369..5ae788d604 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1124,15 +1124,25 @@ impl MessageHandler> for DocumentMes DocumentMode::MaskMode => DocumentMode::DesignMode, _ => DocumentMode::MaskMode, }; + responses.add(PortfolioMessage::UpdateDocumentWidgets); } DocumentMessage::SetDocumentMode { document_mode } => { - self.document_mode = document_mode; + if self.document_mode != document_mode { + self.document_mode = document_mode; + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } } DocumentMessage::EnterMaskMode => { - self.document_mode = DocumentMode::MaskMode; + if self.document_mode != DocumentMode::MaskMode { + self.document_mode = DocumentMode::MaskMode; + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } } DocumentMessage::ExitMaskMode { discard: _ } => { - self.document_mode = DocumentMode::DesignMode; + if self.document_mode != DocumentMode::DesignMode { + self.document_mode = DocumentMode::DesignMode; + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } } DocumentMessage::DrawMarchingAntsOverlay { context: _ } => {} DocumentMessage::AddTransaction => { diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 3238520737..0f314acf04 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -39,16 +39,16 @@ pub enum DocumentMode { MaskMode, } -// impl fmt::Display for DocumentMode { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// match self { -// DocumentMode::DesignMode => write!(f, "Design Mode"), -// DocumentMode::SelectMode => write!(f, "Select Mode"), -// DocumentMode::GuideMode => write!(f, "Guide Mode"), -// DocumentMode::MaskMode => write!(f, "Mask Mode"), -// } -// } -// } +impl fmt::Display for DocumentMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DocumentMode::DesignMode => write!(f, "Design Mode"), + DocumentMode::SelectMode => write!(f, "Select Mode"), + DocumentMode::GuideMode => write!(f, "Guide Mode"), + DocumentMode::MaskMode => write!(f, "Mask Mode"), + } + } +} // impl DocumentMode { // pub fn icon_name(&self) -> String { From cbb615e026b33ca4cb39e9392ab9699e73b24b39 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Tue, 14 Apr 2026 19:11:02 +0530 Subject: [PATCH 5/6] add enter and exit maskMode --- .../document/document_message_handler.rs | 83 +++++++++++++++++-- .../graph_operation_message.rs | 6 ++ .../graph_operation_message_handler.rs | 11 +++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5ae788d604..3b09982a8f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -119,6 +119,13 @@ pub struct DocumentMessageHandler { /// The current editor-only mode for the active document. #[serde(skip)] pub document_mode: DocumentMode, + /// The NodeId of the mask group layer when in MaskMode, or None otherwise. + #[serde(skip)] + pub mask_group_id: Option, + /// The layers that are targets for the current mask operation. + /// When entering MaskMode, this stores the selected layers so the mask can be applied to them on exit. + #[serde(skip)] + pub mask_target_layers: Vec, /// The path of the to the document file. #[serde(skip)] pub(crate) path: Option, @@ -178,6 +185,8 @@ impl Default for DocumentMessageHandler { // ============================================= name: DEFAULT_DOCUMENT_NAME.to_string(), document_mode: DocumentMode::default(), + mask_group_id: None, + mask_target_layers: Vec::new(), path: None, breadcrumb_network_path: Vec::new(), selection_network_path: Vec::new(), @@ -1133,16 +1142,69 @@ impl MessageHandler> for DocumentMes } } DocumentMessage::EnterMaskMode => { - if self.document_mode != DocumentMode::MaskMode { - self.document_mode = DocumentMode::MaskMode; - responses.add(PortfolioMessage::UpdateDocumentWidgets); + if self.document_mode == DocumentMode::MaskMode { + return; } + + self.document_mode = DocumentMode::MaskMode; + + // Store the currently selected layers as mask targets + self.mask_target_layers = self.network_interface.selected_nodes().selected_layers(self.network_interface.document_metadata()).collect(); + + // Create a new group layer for the mask + let mask_group_id = NodeId::new(); + self.mask_group_id = Some(mask_group_id); + + responses.add(DocumentMessage::AddTransaction); + + // Create the mask group layer as an empty custom layer container + graph_modification_utils::new_custom(mask_group_id, Vec::new(), self.new_layer_parent(true), responses); + + // Set the mask group opacity to 50% so the user can see artwork beneath + responses.add(GraphOperationMessage::OpacitySet { + layer: LayerNodeIdentifier::new_unchecked(mask_group_id), + opacity: 0.5, + }); + + responses.add(DocumentMessage::EndTransaction); + responses.add(PortfolioMessage::UpdateDocumentWidgets); } - DocumentMessage::ExitMaskMode { discard: _ } => { - if self.document_mode != DocumentMode::DesignMode { - self.document_mode = DocumentMode::DesignMode; - responses.add(PortfolioMessage::UpdateDocumentWidgets); + DocumentMessage::ExitMaskMode { discard } => { + if self.document_mode != DocumentMode::MaskMode { + return; } + + self.document_mode = DocumentMode::DesignMode; + + if discard { + // Delete the mask group without applying it + if let Some(mask_group_id) = self.mask_group_id { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::DeleteNodes { + node_ids: vec![mask_group_id], + delete_children: true, + }); + responses.add(DocumentMessage::EndTransaction); + } + } else { + // Rasterize the mask group and apply it to target layers + if let Some(mask_group_id) = self.mask_group_id { + responses.add(GraphOperationMessage::ApplyMaskStencil { + layers: self.mask_target_layers.clone(), + mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE), + }); + responses.add(NodeGraphMessage::DeleteNodes { + node_ids: vec![mask_group_id], + delete_children: true, + }); + } + } + + // Clear mask state + self.mask_group_id = None; + self.mask_target_layers.clear(); + + responses.add(PortfolioMessage::UpdateDocumentWidgets); } DocumentMessage::DrawMarchingAntsOverlay { context: _ } => {} DocumentMessage::AddTransaction => { @@ -2116,6 +2178,13 @@ impl DocumentMessageHandler { /// Finds the parent folder which, based on the current selections, should be the container of any newly added layers. pub fn new_layer_parent(&self, include_self: bool) -> LayerNodeIdentifier { + // If we're in MaskMode and have a mask group, all new layers should go into the mask group + if self.document_mode == DocumentMode::MaskMode + && let Some(mask_group_id) = self.mask_group_id + { + return LayerNodeIdentifier::new_unchecked(mask_group_id); + } + let Some(selected_nodes) = self.network_interface.selected_nodes_in_nested_network(&self.selection_network_path) else { warn!("No selected nodes found in new_layer_parent. Defaulting to ROOT_PARENT."); return LayerNodeIdentifier::ROOT_PARENT; diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 14b7dfd66b..30a1dfe5c3 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -14,6 +14,8 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::PointId; use graphene_std::vector::VectorModificationType; use graphene_std::vector::style::{Fill, Stroke}; +use graphene_std::raster::Image; +use graphene_std::Color; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -126,4 +128,8 @@ pub enum GraphOperationMessage { /// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow). center: bool, }, + ApplyMaskStencil { + layers: Vec, + mask_image: Image, + }, } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 46d9116483..b5b5d4e8da 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -439,6 +439,17 @@ impl MessageHandler> for // (skipped automatically when identity, so file-open with content at origin creates no Transform node). modify_inputs.transform_set(placement_transform, TransformIn::Local, false); } + GraphOperationMessage::ApplyMaskStencil { layers, mask_image } => { + let _ = mask_image; + + // For each target layer, toggle clip mode so the existing clip infrastructure is engaged. + for layer in layers { + responses.add(GraphOperationMessage::ClipModeToggle { layer }); + } + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(NodeGraphMessage::SelectedNodesUpdated); + responses.add(NodeGraphMessage::SendGraph); + } } } From 5ca8e961820809c1db2dfe1177acb30e799e8743 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Wed, 15 Apr 2026 19:39:59 +0530 Subject: [PATCH 6/6] keep mask group content and transaction flow intact --- .../document/document_message_handler.rs | 18 +++++++----------- .../graph_operation/graph_operation_message.rs | 3 --- .../graph_operation_message_handler.rs | 4 +--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 3b09982a8f..fea5a6de71 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1150,6 +1150,7 @@ impl MessageHandler> for DocumentMes // Store the currently selected layers as mask targets self.mask_target_layers = self.network_interface.selected_nodes().selected_layers(self.network_interface.document_metadata()).collect(); + let layer_parent = self.new_layer_parent(true); // Create a new group layer for the mask let mask_group_id = NodeId::new(); @@ -1158,7 +1159,7 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::AddTransaction); // Create the mask group layer as an empty custom layer container - graph_modification_utils::new_custom(mask_group_id, Vec::new(), self.new_layer_parent(true), responses); + graph_modification_utils::new_custom(mask_group_id, Vec::new(), layer_parent, responses); // Set the mask group opacity to 50% so the user can see artwork beneath responses.add(GraphOperationMessage::OpacitySet { @@ -1187,16 +1188,11 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::EndTransaction); } } else { - // Rasterize the mask group and apply it to target layers - if let Some(mask_group_id) = self.mask_group_id { - responses.add(GraphOperationMessage::ApplyMaskStencil { - layers: self.mask_target_layers.clone(), - mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE), - }); - responses.add(NodeGraphMessage::DeleteNodes { - node_ids: vec![mask_group_id], - delete_children: true, - }); + // Apply the mask group to the target layers + if self.mask_group_id.is_some() { + responses.add(DocumentMessage::AddTransaction); + responses.add(GraphOperationMessage::ApplyMaskStencil { layers: self.mask_target_layers.clone() }); + responses.add(DocumentMessage::EndTransaction); } } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 30a1dfe5c3..fe583e6c0b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -14,8 +14,6 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::PointId; use graphene_std::vector::VectorModificationType; use graphene_std::vector::style::{Fill, Stroke}; -use graphene_std::raster::Image; -use graphene_std::Color; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -130,6 +128,5 @@ pub enum GraphOperationMessage { }, ApplyMaskStencil { layers: Vec, - mask_image: Image, }, } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index b5b5d4e8da..06d342f3d3 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -439,9 +439,7 @@ impl MessageHandler> for // (skipped automatically when identity, so file-open with content at origin creates no Transform node). modify_inputs.transform_set(placement_transform, TransformIn::Local, false); } - GraphOperationMessage::ApplyMaskStencil { layers, mask_image } => { - let _ = mask_image; - + GraphOperationMessage::ApplyMaskStencil { layers } => { // For each target layer, toggle clip mode so the existing clip infrastructure is engaged. for layer in layers { responses.add(GraphOperationMessage::ClipModeToggle { layer });