From d44b0cd308b1da32a54d9787cd48715c6d808631 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 18:37:13 -0700 Subject: [PATCH] Make documents fall back to "Untitled Document" (+ number suffix) if given a blank name (#4074) * Make documents fall back to "Untitled Document" (+ number suffix) if given a blank name * Bug fix --- .../messages/dialog/dialog_message_handler.rs | 2 +- .../document/document_message_handler.rs | 10 ++++- .../node_graph/node_graph_message_handler.rs | 2 +- .../messages/portfolio/portfolio_message.rs | 3 ++ .../portfolio/portfolio_message_handler.rs | 44 ++++++++++++------- frontend/wrapper/src/editor_wrapper.rs | 2 +- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 29009bfd..e788ff53 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -126,7 +126,7 @@ impl MessageHandler> for DialogMessageHa DialogMessage::RequestNewDocumentDialog => { self.on_dismiss = Some(DialogMessage::Close.into()); self.new_document_dialog = NewDocumentDialogMessageHandler { - name: portfolio.generate_new_document_name(), + name: portfolio.generate_new_document_name(None), infinite: false, dimensions: glam::UVec2::new(1920, 1080), }; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index dae0f1f4..252b2ae5 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -784,7 +784,15 @@ impl MessageHandler> for DocumentMes responses.add(EventMessage::SelectionChanged); } DocumentMessage::RenameDocument { new_name } => { - self.name = new_name.clone(); + let new_name = new_name.trim().to_string(); + + // No-op when the resolved name is unchangedL committing the rename field without edits (or with + // only whitespace edits) shouldn't dissociate the document from its file on disk or mark it unsaved. + if new_name == self.name { + return; + } + + self.name = new_name; self.path = None; self.set_save_state(false); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 17849a6a..e67a7382 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -2475,7 +2475,7 @@ impl NodeGraphMessageHandler { Separator::new(SeparatorStyle::Related).widget_instance(), TextInput::new(context.document_name) .tooltip_description("Name of the current document.") - .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) + .on_update(|text_input| PortfolioMessage::RenameDocument { new_name: text_input.value.clone() }.into()) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), ])]; diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index b5bdc45c..979fae22 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -168,6 +168,9 @@ pub enum PortfolioMessage { SelectDocument { document_id: DocumentId, }, + RenameDocument { + new_name: String, + }, SubmitDocumentExport { name: String, file_type: FileType, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index d4e82c0e..dc404a1a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -498,7 +498,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::NewDocumentWithName { name } => { let mut new_document = DocumentMessageHandler::default(); - new_document.name = name; + new_document.name = self.resolve_document_name(name, None); responses.add(DocumentMessage::PTZUpdate); @@ -799,28 +799,24 @@ impl MessageHandler> for Portfolio } }); - match (document_name, document_path, document_name_from_path) { - (Some(name), _, None) => { - document.name = name; - } + let candidate_name = match (document_name, document_path, document_name_from_path) { + (Some(name), _, None) => name, (_, Some(path), Some(name)) => { - document.name = name; document.path = Some(path); + name } - (_, _, Some(name)) => { - document.name = name; - } - _ => { - document.name = DEFAULT_DOCUMENT_NAME.to_string(); - } - } + (_, _, Some(name)) => name, + _ => String::new(), + }; + document.name = self.resolve_document_name(candidate_name, None); // Load the document into the portfolio so it opens in the editor self.load_document(document, document_id, responses); } PortfolioMessage::OpenImage { name, image } => { + // `NewDocumentWithName`'s handler routes empty/None-equivalent names through `resolve_document_name` which assigns the next available "Untitled Document {N}". responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), + name: name.clone().unwrap_or_default(), }); responses.add(DocumentMessage::PasteImage { @@ -847,7 +843,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::OpenSvg { name, svg } => { responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), + name: name.clone().unwrap_or_default(), }); // Parse the SVG to extract its declared canvas origin and dimensions from the viewBox attribute. @@ -1359,6 +1355,10 @@ impl MessageHandler> for Portfolio self.refresh_panel_content(target_active, responses); } } + PortfolioMessage::RenameDocument { new_name } => { + let resolved_name = self.resolve_document_name(new_name, self.active_document_id); + responses.add(DocumentMessage::RenameDocument { new_name: resolved_name }); + } PortfolioMessage::SelectDocument { document_id } => { // Auto-save the document we are leaving let mut node_graph_open = false; @@ -1744,10 +1744,22 @@ impl PortfolioMessageHandler { } } - pub fn generate_new_document_name(&self) -> String { + /// Resolves a proposed document name: if it's empty or only whitespace, falls back to the next + /// available "Untitled Document {N}" via [`Self::generate_new_document_name`]. Otherwise trims surrounding + /// whitespace and returns it. `exclude` is forwarded so a renaming document can skip its own current + /// name when computing the fallback (preventing self-collision). + pub fn resolve_document_name(&self, name: String, exclude: Option) -> String { + let trimmed = name.trim(); + if trimmed.is_empty() { self.generate_new_document_name(exclude) } else { trimmed.to_string() } + } + + /// `exclude` lets a renaming caller skip its own current name so a document can rename back to its + /// existing slot rather than colliding with itself and getting bumped to the next number. + pub fn generate_new_document_name(&self, exclude: Option) -> String { let mut doc_title_numbers = self .document_ids .iter() + .filter(|id| exclude != Some(**id)) .filter_map(|id| self.document_details(*id)) .filter_map(|doc| { doc.name diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index eeb89caa..26522467 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -410,7 +410,7 @@ impl EditorWrapper { /// Rename the currently active document. #[wasm_bindgen(js_name = renameDocument)] pub fn rename_document(&self, new_name: String) { - let message = DocumentMessage::RenameDocument { new_name }; + let message = PortfolioMessage::RenameDocument { new_name }; self.dispatch(message); }