From bfb0b4bb8ccf9d91607c34d31cc86053b4edc9d0 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 00:31:23 +0000 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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()); From 2ace6e9428ec80e31e26f5b611d2a1e24739b816 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 19:59:14 +0000 Subject: [PATCH 06/13] Address review comments --- .../portfolio/document/document_message_handler.rs | 13 ++----------- .../portfolio/menu_bar/menu_bar_message_handler.rs | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 92a50fb562..338be12e3a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -947,17 +947,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::RenameDocument { 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; - } - } + self.path = None; + self.set_save_state(false); responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::UpdateNewNodeGraph); 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 f962bbf10e..44e8fd63c9 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 @@ -111,7 +111,7 @@ impl LayoutHolder for MenuBarMessageHandler { ..MenuBarEntry::default() }, MenuBarEntry { - label: "Save as".into(), + label: "Save As…".into(), icon: Some("Save".into()), shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs), action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()), From e0cea6633adf6409f77c2583dd2ea008f289371f Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 20:52:15 +0000 Subject: [PATCH 07/13] Replace file save suffix with file extension --- desktop/src/app.rs | 1 - editor/src/consts.rs | 3 +-- .../export_dialog/export_dialog_message_handler.rs | 2 +- .../portfolio/document/document_message_handler.rs | 4 ++-- editor/src/messages/portfolio/portfolio_message.rs | 2 +- .../messages/portfolio/portfolio_message_handler.rs | 4 ++-- editor/src/node_graph_executor.rs | 12 ++---------- editor/src/node_graph_executor/runtime.rs | 2 +- frontend/src/state-providers/portfolio.ts | 6 +++--- frontend/wasm/src/editor_api.rs | 10 +++++----- 10 files changed, 18 insertions(+), 28 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 30d0286f57..98d284f7cf 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -10,7 +10,6 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphene_std::Color; use graphene_std::raster::Image; use graphite_editor::application::Editor; -use graphite_editor::consts::DEFAULT_DOCUMENT_NAME; use graphite_editor::messages::prelude::*; use std::fs; use std::sync::Arc; diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 48bdea853f..0fc75466f7 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -150,9 +150,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; 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 DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; 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/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 0fa126917b..624db0ab9f 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -44,7 +44,7 @@ impl MessageHandler> for Exp ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area, ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport { - file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), + name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), file_type: self.file_type, scale_factor: self.scale_factor, bounds: self.bounds, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 338be12e3a..79d8ab1a36 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_EXTENSION, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, 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::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; @@ -1034,7 +1034,7 @@ impl MessageHandler> for DocumentMes responses.add(FrontendMessage::TriggerSaveDocument { document_id, - name: self.name.clone() + FILE_SAVE_SUFFIX, + name: format!("{}.{}", self.name.clone(), FILE_EXTENSION), 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 0e47d254a4..75717bac42 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -118,7 +118,7 @@ pub enum PortfolioMessage { document_id: DocumentId, }, SubmitDocumentExport { - file_name: String, + name: String, file_type: FileType, scale_factor: f64, bounds: ExportBounds, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index aef9738a30..6bbf996474 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -923,7 +923,7 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::SubmitDocumentExport { - file_name, + name, file_type, scale_factor, bounds, @@ -931,7 +931,7 @@ impl MessageHandler> for Portfolio } => { let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document"); let export_config = ExportConfig { - file_name, + name, file_type, scale_factor, bounds, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 560ff47188..b3584d342d 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,4 +1,3 @@ -use crate::consts::FILE_SAVE_SUFFIX; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::prelude::*; use glam::{DAffine2, DVec2, UVec2}; @@ -232,18 +231,11 @@ impl NodeGraphExecutor { }; let ExportConfig { - file_type, - file_name, - size, - scale_factor, - .. + file_type, name, size, scale_factor, .. } = export_config; let file_suffix = &format!(".{file_type:?}").to_lowercase(); - let name = match file_name.ends_with(FILE_SAVE_SUFFIX) { - true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix), - false => file_name + file_suffix, - }; + let name = name + file_suffix; if file_type == FileType::Svg { responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 55a262ab2c..0838f1781c 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -74,7 +74,7 @@ pub struct GraphUpdate { #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ExportConfig { - pub file_name: String, + pub name: String, pub file_type: FileType, pub scale_factor: f64, pub bounds: ExportBounds, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 41ad207f02..5b3178e9a2 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -62,8 +62,8 @@ export function createPortfolioState(editor: Editor) { } }); editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => { - const extension = editor.handle.fileSaveSuffix(); - const data = await upload(extension, "text"); + const extension = editor.handle.fileExtension(); + const data = await upload("." + extension, "text"); editor.handle.openDocumentFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { @@ -76,7 +76,7 @@ export function createPortfolioState(editor: Editor) { } // In case the user accidentally uploads a Graphite file, open it instead of failing to import it - if (data.filename.endsWith(".graphite")) { + if (data.filename.endsWith("." + editor.handle.fileExtension())) { editor.handle.openDocumentFile(data.filename, data.content.text); return; } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 501e96cde0..9376d7c4ed 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -6,7 +6,7 @@ // use crate::helpers::translate_key; use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; -use editor::consts::FILE_SAVE_SUFFIX; +use editor::consts::FILE_EXTENSION; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -344,10 +344,10 @@ impl EditorHandle { cfg!(debug_assertions) } - /// Get the constant `FILE_SAVE_SUFFIX` - #[wasm_bindgen(js_name = fileSaveSuffix)] - pub fn file_save_suffix(&self) -> String { - FILE_SAVE_SUFFIX.into() + /// Get the constant `FILE_EXTENSION` + #[wasm_bindgen(js_name = fileExtension)] + pub fn file_extension(&self) -> String { + FILE_EXTENSION.into() } /// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that) From 18e73605d083d8b129e0e85afd28c89322867ac7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 20:53:42 +0000 Subject: [PATCH 08/13] Add comment specifying that the upload function takes a html input accept string --- frontend/src/utility-functions/files.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index 6d230433a1..08be7ccdf2 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -22,11 +22,12 @@ export function downloadFile(filename: string, content: ArrayBuffer) { downloadFileBlob(filename, blob); } -export async function upload(acceptedExtensions: string, textOrData: T): Promise> { +// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format +export async function upload(accept: string, textOrData: T): Promise> { return new Promise>((resolve, _) => { const element = document.createElement("input"); element.type = "file"; - element.accept = acceptedExtensions; + element.accept = accept; element.addEventListener( "change", From a44de05c80a8dfa03221192fe9e8c79ea69fabfd Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 21:04:43 +0000 Subject: [PATCH 09/13] Fix remove file extension in web --- frontend/src/state-providers/portfolio.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 5b3178e9a2..220d7daf22 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -62,9 +62,16 @@ export function createPortfolioState(editor: Editor) { } }); editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => { - const extension = editor.handle.fileExtension(); - const data = await upload("." + extension, "text"); - editor.handle.openDocumentFile(data.filename, data.content); + const suffix = "." + editor.handle.fileExtension(); + const data = await upload(suffix, "text"); + + // Use filename as document name, removing the extension if it exists + var documentName = data.filename; + if (documentName.endsWith(suffix)) { + documentName = documentName.slice(0, -suffix.length); + } + + editor.handle.openDocumentFile(documentName, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { const data = await upload("image/*", "both"); From e708348143697be54d0d488cb825f31117e98a7d Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 21:20:18 +0000 Subject: [PATCH 10/13] Use let --- frontend/src/state-providers/portfolio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 220d7daf22..288bf4fcb9 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -66,7 +66,7 @@ export function createPortfolioState(editor: Editor) { const data = await upload(suffix, "text"); // Use filename as document name, removing the extension if it exists - var documentName = data.filename; + let documentName = data.filename; if (documentName.endsWith(suffix)) { documentName = documentName.slice(0, -suffix.length); } From a6d9de8b6b71d4aa8d51824282f7a7140956fc97 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 21:32:32 +0000 Subject: [PATCH 11/13] Don't show save as menu entry in web --- .../src/messages/portfolio/menu_bar/menu_bar_message_handler.rs | 1 + 1 file changed, 1 insertion(+) 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 44e8fd63c9..9b86761d87 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 @@ -110,6 +110,7 @@ impl LayoutHolder for MenuBarMessageHandler { disabled: no_active_document, ..MenuBarEntry::default() }, + #[cfg(not(target_family = "wasm"))] MenuBarEntry { label: "Save As…".into(), icon: Some("Save".into()), From 250921b2df040c90ed9407d9cd95e8c976c4e209 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 21:49:47 +0000 Subject: [PATCH 12/13] Don't add SaveDocumentAs in web --- .../messages/portfolio/document/document_message_handler.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 65dd800a04..368e2f0fe7 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1575,7 +1575,6 @@ impl MessageHandler> for DocumentMes Noop, Redo, SaveDocument, - SaveDocumentAs, SelectAllLayers, SetSnapping, ToggleGridVisibility, @@ -1590,6 +1589,10 @@ impl MessageHandler> for DocumentMes ZoomCanvasToFitAll, ); + // Additional actions available on desktop + #[cfg(not(target_family = "wasm"))] + common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs)); + // Additional actions if there are any selected layers if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() { let mut select = actions!(DocumentMessageDiscriminant; From 6e26af954a82e4dee338eb127243f38676f94321 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 20 Aug 2025 09:47:03 +0000 Subject: [PATCH 13/13] Remove file extension on all open document file calls --- frontend/src/components/panels/Document.svelte | 6 ++++-- frontend/src/components/panels/Layers.svelte | 6 ++++-- frontend/src/components/window/workspace/Panel.svelte | 6 ++++-- frontend/src/io-managers/input.ts | 7 +++++-- frontend/src/state-providers/portfolio.ts | 6 ++++-- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index b907b51c85..22c7862438 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -151,9 +151,11 @@ return; } - if (file.name.endsWith(".graphite")) { + const graphiteFileSuffix = "." + editor.handle.fileExtension(); + if (file.name.endsWith(graphiteFileSuffix)) { const content = await file.text(); - editor.handle.openDocumentFile(file.name, content); + const documentName = file.name.slice(0, -graphiteFileSuffix.length); + editor.handle.openDocumentFile(documentName, content); return; } }); diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 36159736a9..2f3626797b 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -438,9 +438,11 @@ } // When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab - if (file.name.endsWith(".graphite")) { + const graphiteFileSuffix = "." + editor.handle.fileExtension(); + if (file.name.endsWith(graphiteFileSuffix)) { const content = await file.text(); - editor.handle.openDocumentFile(file.name, content); + const documentName = file.name.slice(0, -graphiteFileSuffix.length); + editor.handle.openDocumentFile(documentName, content); return; } }); diff --git a/frontend/src/components/window/workspace/Panel.svelte b/frontend/src/components/window/workspace/Panel.svelte index f53b9ff399..958fe702fb 100644 --- a/frontend/src/components/window/workspace/Panel.svelte +++ b/frontend/src/components/window/workspace/Panel.svelte @@ -83,9 +83,11 @@ return; } - if (file.name.endsWith(".graphite")) { + const graphiteFileSuffix = "." + editor.handle.fileExtension(); + if (file.name.endsWith(graphiteFileSuffix)) { const content = await file.text(); - editor.handle.openDocumentFile(file.name, content); + const documentName = file.name.slice(0, -graphiteFileSuffix.length); + editor.handle.openDocumentFile(documentName, content); return; } }); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 711e039804..f0ac34db0c 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -334,8 +334,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height); } - if (file.name.endsWith(".graphite")) { - editor.handle.openDocumentFile(file.name, await file.text()); + const graphiteFileSuffix = "." + editor.handle.fileExtension(); + if (file.name.endsWith(graphiteFileSuffix)) { + const content = await file.text(); + const documentName = file.name.slice(0, -graphiteFileSuffix.length); + editor.handle.openDocumentFile(documentName, content); } }); } diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 288bf4fcb9..0c3d5ad364 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -83,8 +83,10 @@ export function createPortfolioState(editor: Editor) { } // In case the user accidentally uploads a Graphite file, open it instead of failing to import it - if (data.filename.endsWith("." + editor.handle.fileExtension())) { - editor.handle.openDocumentFile(data.filename, data.content.text); + const graphiteFileSuffix = "." + editor.handle.fileExtension(); + if (data.filename.endsWith(graphiteFileSuffix)) { + const documentName = data.filename.slice(0, -graphiteFileSuffix.length); + editor.handle.openDocumentFile(documentName, data.content.text); return; }