From bfb0b4bb8ccf9d91607c34d31cc86053b4edc9d0 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 00:31:23 +0000 Subject: [PATCH 1/5] Make file name and document name identical --- desktop/src/app.rs | 3 +- editor/src/consts.rs | 1 + .../document/document_message_handler.rs | 38 ++++++++++++------- .../messages/portfolio/portfolio_message.rs | 9 +++-- .../portfolio/portfolio_message_handler.rs | 34 ++++++++++++++--- frontend/wasm/src/editor_api.rs | 6 ++- 6 files changed, 66 insertions(+), 25 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0ec9eaa81e..05fd1db043 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -75,7 +75,8 @@ impl WinitApp { String::new() }); let message = PortfolioMessage::OpenDocumentFile { - document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(), + document_name: None, + document_path: Some(path), document_serialized_content: content, }; let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into())); diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 44a5b1d210..48bdea853f 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -151,6 +151,7 @@ pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; // DOCUMENT pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; +pub const FILE_EXTENSION: &str = "graphite"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5bfa0af7dd..7107e28ae9 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; -use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -76,8 +76,6 @@ pub struct DocumentMessageHandler { /// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel. /// Collapsed means that the expansion arrow isn't set to show the children of these layers. pub collapsed: CollapsedLayers, - /// The name of the document, which is displayed in the tab and title bar of the editor. - pub name: String, /// The full Git commit hash of the Graphite repository that was used to build the editor. /// We save this to provide a hint about which version of the editor was used to create the document. pub commit_hash: String, @@ -104,6 +102,12 @@ pub struct DocumentMessageHandler { // Fields omitted from the saved document format // ============================================= // + /// The name of the document, which is displayed in the tab and title bar of the editor. + #[serde(skip)] + pub name: String, + /// The path of the to the document file. + #[serde(skip)] + pub(crate) path: Option, /// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks #[serde(skip)] breadcrumb_network_path: Vec, @@ -116,9 +120,6 @@ pub struct DocumentMessageHandler { /// Stack of document network snapshots for future history states. #[serde(skip)] document_redo_history: VecDeque, - /// The path of the to the document file. - #[serde(skip)] - path: Option, /// Hash of the document snapshot that was most recently saved to disk by the user. #[serde(skip)] saved_hash: Option, @@ -149,7 +150,6 @@ impl Default for DocumentMessageHandler { // ============================================ network_interface: default_document_network_interface(), collapsed: CollapsedLayers::default(), - name: DEFAULT_DOCUMENT_NAME.to_string(), commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(), document_ptz: PTZ::default(), document_mode: DocumentMode::DesignMode, @@ -162,11 +162,12 @@ impl Default for DocumentMessageHandler { // ============================================= // Fields omitted from the saved document format // ============================================= + name: DEFAULT_DOCUMENT_NAME.to_string(), + path: None, breadcrumb_network_path: Vec::new(), selection_network_path: Vec::new(), document_undo_history: VecDeque::new(), document_redo_history: VecDeque::new(), - path: None, saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, @@ -918,7 +919,20 @@ impl MessageHandler> for DocumentMes responses.add(OverlaysMessage::Draw); } DocumentMessage::RenameDocument { new_name } => { - self.name = new_name; + self.name = new_name.clone(); + + // If the new document name does not match the current path, clear the path. + if let Some(path) = self.path.as_ref() { + let document_name_from_path = if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + }; + if Some(new_name) != document_name_from_path { + self.path = None; + } + } + responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::UpdateNewNodeGraph); } @@ -997,13 +1011,9 @@ impl MessageHandler> for DocumentMes // Update the save status of the just saved document responses.add(PortfolioMessage::UpdateOpenDocumentsList); - let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { - true => self.name.clone(), - false => self.name.clone() + FILE_SAVE_SUFFIX, - }; responses.add(FrontendMessage::TriggerSaveDocument { document_id, - name, + name: self.name.clone() + FILE_SAVE_SUFFIX, path: self.path.clone(), content: self.serialize_document().into_bytes(), }) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 153ad6c039..58be6ab13e 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -6,6 +6,7 @@ use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::raster::Image; use graphene_std::text::Font; +use std::path::PathBuf; #[impl_message(Message, Portfolio)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -68,18 +69,20 @@ pub enum PortfolioMessage { NextDocument, OpenDocument, OpenDocumentFile { - document_name: String, + document_name: Option, + document_path: Option, document_serialized_content: String, }, - ToggleResetNodesToDefinitionsOnOpen, OpenDocumentFileWithId { document_id: DocumentId, - document_name: String, + document_name: Option, + document_path: Option, document_is_auto_saved: bool, document_is_saved: bool, document_serialized_content: String, to_front: bool, }, + ToggleResetNodesToDefinitionsOnOpen, PasteIntoFolder { clipboard: Clipboard, parent: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54ed..0097ad61fb 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -3,7 +3,7 @@ use super::document::utility_types::network_interface; use super::spreadsheet::SpreadsheetMessageHandler; use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; -use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH}; +use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::dialog::simple_dialogs; @@ -408,12 +408,14 @@ impl MessageHandler> for Portfolio } PortfolioMessage::OpenDocumentFile { document_name, + document_path, document_serialized_content, } => { let document_id = DocumentId(generate_uuid()); responses.add(PortfolioMessage::OpenDocumentFileWithId { document_id, document_name, + document_path, document_is_auto_saved: false, document_is_saved: true, document_serialized_content, @@ -428,6 +430,7 @@ impl MessageHandler> for Portfolio PortfolioMessage::OpenDocumentFileWithId { document_id, document_name, + document_path, document_is_auto_saved, document_is_saved, document_serialized_content, @@ -439,10 +442,7 @@ impl MessageHandler> for Portfolio let document_serialized_content = document_migration_string_preprocessing(document_serialized_content); // Deserialize the document - let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| { - document.name.clone_from(&document_name); - document - }); + let document = DocumentMessageHandler::deserialize_document(&document_serialized_content); // Display an error to the user if the document could not be opened let mut document = match document { @@ -503,6 +503,30 @@ impl MessageHandler> for Portfolio document.set_auto_save_state(document_is_auto_saved); document.set_save_state(document_is_saved); + let document_name_from_path = document_path.as_ref().and_then(|path| { + if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + } + }); + + match (document_name, document_path, document_name_from_path) { + (Some(name), _, None) => { + document.name = name; + } + (_, Some(path), Some(name)) => { + document.name = name; + document.path = Some(path); + } + (_, _, Some(name)) => { + document.name = name; + } + _ => { + document.name = DEFAULT_DOCUMENT_NAME.to_string(); + } + } + // Load the document into the portfolio so it opens in the editor self.load_document(document, document_id, responses, to_front); } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 2b81d98b95..cc52877ec7 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -425,7 +425,8 @@ impl EditorHandle { #[wasm_bindgen(js_name = openDocumentFile)] pub fn open_document_file(&self, document_name: String, document_serialized_content: String) { let message = PortfolioMessage::OpenDocumentFile { - document_name, + document_name: Some(document_name), + document_path: None, document_serialized_content, }; self.dispatch(message); @@ -436,7 +437,8 @@ impl EditorHandle { let document_id = DocumentId(document_id); let message = PortfolioMessage::OpenDocumentFileWithId { document_id, - document_name, + document_name: Some(document_name), + document_path: None, document_is_auto_saved: true, document_is_saved, document_serialized_content, From d78317d10add592b8e420c1d47a024307381d770 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 01:49:41 +0000 Subject: [PATCH 2/5] Add save as action --- .../messages/input_mapper/input_mappings.rs | 1 + .../portfolio/document/document_message.rs | 1 + .../document/document_message_handler.rs | 21 ++++++++++++++- .../menu_bar/menu_bar_message_handler.rs | 26 +++++++++++++------ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 1b722301e6..6eb053bfde 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers), entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers), entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument), + entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs), entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers), entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers), entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 89db87c983..b6eb9a4fd6 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -107,6 +107,7 @@ pub enum DocumentMessage { RenderRulers, RenderScrollbars, SaveDocument, + SaveDocumentAs, SavedDocument { path: Option, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7107e28ae9..ee329860ac 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1005,7 +1005,11 @@ impl MessageHandler> for DocumentMes multiplier: scrollbar_multiplier.into(), }); } - DocumentMessage::SaveDocument => { + DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => { + if let DocumentMessage::SaveDocumentAs = message { + self.path = None; + } + self.set_save_state(true); responses.add(PortfolioMessage::AutoSaveActiveDocument); // Update the save status of the just saved document @@ -1020,6 +1024,21 @@ impl MessageHandler> for DocumentMes } DocumentMessage::SavedDocument { path } => { self.path = path; + + // Update the name to match the file stem + let document_name_from_path = self.path.as_ref().and_then(|path| { + if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + } + }); + if let Some(name) = document_name_from_path { + self.name = name; + + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + responses.add(NodeGraphMessage::UpdateNewNodeGraph); + } } DocumentMessage::SelectParentLayer => { let selected_nodes = self.network_interface.selected_nodes(); diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 0f4b574de8..3aab220345 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -99,14 +99,24 @@ impl LayoutHolder for MenuBarMessageHandler { ..MenuBarEntry::default() }, ], - vec![MenuBarEntry { - label: "Save".into(), - icon: Some("Save".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument), - action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }], + vec![ + MenuBarEntry { + label: "Save".into(), + icon: Some("Save".into()), + shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument), + action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()), + disabled: no_active_document, + ..MenuBarEntry::default() + }, + MenuBarEntry { + label: "Save as".into(), + icon: Some("Save".into()), + shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs), + action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()), + disabled: no_active_document, + ..MenuBarEntry::default() + }, + ], vec![ MenuBarEntry { label: "Import…".into(), From b63d36f5f1562b10713d92ae05818968176d76f2 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 16:17:17 +0000 Subject: [PATCH 3/5] Fix test errors --- editor/src/dispatcher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 0c098833a3..4073a46deb 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -501,7 +501,8 @@ mod test { ); let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile { - document_name: document_name.into(), + document_name: Some(document_name.to_string()), + document_path: None, document_serialized_content, }); From ec8fbfb84a3734786a2469b3ad825850504806d2 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 11 Aug 2025 11:52:20 +0200 Subject: [PATCH 4/5] Add missing save as action --- .../src/messages/portfolio/document/document_message_handler.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index ee329860ac..e5d2c1ffb9 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1556,6 +1556,7 @@ impl MessageHandler> for DocumentMes Noop, Redo, SaveDocument, + SaveDocumentAs, SelectAllLayers, SetSnapping, ToggleGridVisibility, From 6a5897a06494e60cbef1992c44d2521618f5faa7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 18:03:30 +0000 Subject: [PATCH 5/5] Desktop fix drop file open document file message --- desktop/src/app.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 5b425182d2..30d0286f57 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -295,7 +295,8 @@ impl ApplicationHandler for WinitApp { let Some(content) = load_string(&path) else { return }; let message = PortfolioMessage::OpenDocumentFile { - document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()), + document_name: None, + document_path: Some(path), document_serialized_content: content, }; self.dispatch_message(message.into());