From a05dca2e9d6209d54e3fef41769dd3b29fba7908 Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Sat, 14 Nov 2026 22:02:18 +0900 Subject: [PATCH] Fix #7275: Commit artboard names when switching export tabs --- .../ui-egui/src/dialogs/export_for_screens.rs | 32 ++++++++++++++----- crates/ui-egui/src/dialogs/tests_screens.rs | 32 +++++++++++++++++++ docs/target-app-parity.md | 1 + 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 8957ca0fa9..120b1164be 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -391,7 +391,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.vertical(|ui| { ui.set_width(LEFT_W); if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) { - d.fields.remove(RENAME_ARTBOARD); + if i != tab + && let Some((index, text, _)) = take_artboard_edit(d) + { + rename_artboard(app, index, &text); + } d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards"))); } ui.add_space(6.0); @@ -521,7 +525,7 @@ fn select_options(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) { let mut boards = boards(app, d); let before = boards.clone(); - let mut editing: Option<(usize, String, bool)> = d.fields.remove(RENAME_ARTBOARD).and_then(|value| serde_json::from_value(value).ok()); + let mut editing = take_artboard_edit(d); let mut rename = None; let names: Vec = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); ui.add_enabled_ui(d.str("select") != "full", |ui| { @@ -559,10 +563,8 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l { d.fields.insert(RENAME_ARTBOARD.into(), json!(edit)); } - if let Some((i, name)) = rename - && let Err(err) = app.run("artboard.rename", json!({"indices": [i], "name": name})) - { - app.status(err); + if let Some((i, name)) = rename { + rename_artboard(app, i, &name); } if boards != before { set_select(d, if boards.iter().all(|b| *b) { "all" } else { "range" }); @@ -574,6 +576,20 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l /// Inline editor state belongs to this dialog, not to the document or its export settings. pub(super) const RENAME_ARTBOARD: &str = "__renameArtboard"; +fn take_artboard_edit(d: &mut Dialog) -> Option<(usize, String, bool)> { + d.fields.remove(RENAME_ARTBOARD).and_then(|value| serde_json::from_value(value).ok()) +} + +fn rename_artboard(app: &mut VectorcraftApp, index: usize, text: &str) { + let name = text.trim(); + if !name.is_empty() + && app.session.active().and_then(|s| s.doc.artboards.get(index)).is_some_and(|a| a.name != name) + && let Err(err) = app.run("artboard.rename", json!({"indices": [index], "name": name})) + { + app.status(err); + } +} + fn edit_artboard_name( ui: &mut egui::Ui, index: usize, @@ -595,8 +611,8 @@ fn edit_artboard_name( widgets::focus_field(ui, field, text); *focus = false; } else if cancel || enter || response.lost_focus() { - if !cancel && !text.trim().is_empty() && text.trim() != name { - *rename = Some((*i, text.trim().into())); + if !cancel { + *rename = Some((*i, std::mem::take(text))); } response.surrender_focus(); *editing = None; diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index 7baa4d5899..a361d4290b 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1320,6 +1320,38 @@ fn the_dialog_builds_the_preset_list_again_only_when_the_presets_change() { assert_eq!(mine["settings"]["formats"][0]["format"], "pdf", "{list}"); } +/// #7275: a double-click on an artboard name enters rename without leaving Export for Screens. +#[test] +fn switching_export_tabs_commits_an_artboard_name() { + for view in ["grid", "list"] { + let (mut app, log) = app(2, true); + open(&mut app); + set(&mut app, "view", json!(view)); + let mut h = ScreensFrame::new(); + let before = field(&app, "boards"); + let out = h.settle(&mut app); + let old = app.session.active().unwrap().doc.artboards[0].name.clone(); + let at = screen_text_at(&out, &format!("1 {old}")); + h.frame(&mut app, vec![egui::Event::PointerMoved(at)]); + for pressed in [true, false, true, false] { + h.frame( + &mut app, + vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }], + ); + } + h.settle(&mut app); + assert!(h.ctx.egui_wants_keyboard_input()); + h.frame(&mut app, vec![egui::Event::Text("Committed on tab".into())]); + h.click_label(&mut app, "Assets"); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Committed on tab"); + assert_eq!(field(&app, "tab"), "assets"); + assert_eq!(field(&app, "boards"), before); + assert!(log.borrow().is_empty()); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, old); + } +} + /// #7275: a double-click on an artboard name enters rename without leaving Export for Screens. #[test] fn double_clicking_an_artboard_name_in_export_for_screens_renames_it() { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8c5bc6de38..ce2345f56d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -799,6 +799,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Batch… over data sets offers None, Save and Close and Folder as the destination, as for a folder source. Save and Close saves the open document over its own file after each data set, so the file keeps only the last data set, unless a recorded Save step that Override Save points into the destination folder first makes each data set's file the document's own. A recorded Save, Save a Copy, Save as Template or export step that writes a file and that the batch does not point into the destination folder (destination None, Override Save or Override Export off, or a copy, template, PDF, EPS, DXF, Save for Web or Save for Office Documents step) also writes the same file for every data set. In either case the dialog shows a warning and `action.batch` returns a `warning`. Unless a recorded Save or Save As step writes a file and makes it the document's own first, `action.batch` refuses Save and Close over the data sets of a document that was never saved, was converted from an older version or was restored by Data Recovery. - Export for Screens: the dialog's Example line shows the path of the file the export writes first, sub-folder included, named as the export names it, for the first chosen artboard, asset, layer or object (`document.exportForScreens`). - Export presets: the Asset Export panel menu lists the export presets of Export for Screens, with Import Preset… and Export Preset…, which read and write `.vcexportpresets` files (`document.exportPresets.import`, `document.exportPresets.export`). An imported preset whose name is taken gets a number. In the panel, a preset applies to the shared export settings (`assets.settings.set {exportPreset}`), and Save Preset… saves them. With Create Sub-folders off, the panel's Export writes every file into the folder itself, mobile and density rows included. + - Export for Screens artboard names can be double-clicked to rename them in grid or list view (#7275). Enter, a click outside the field or switching to Assets commits the name; Escape cancels the edit. The dialog stays open, checked artboards stay selected and the rename is undoable. Evidence: `dialogs::tests_screens::double_clicking_an_artboard_name_in_export_for_screens_renames_it` and `dialogs::tests_screens::switching_export_tabs_commits_an_artboard_name`. - Export for Screens is laid out as the reference app's dialog: on the left the Artboards and Assets tabs (thumbnails with their checkboxes, number and name, in a grid or a list), Clear Selection and Prefix; on the right Select (All, Range, Include Bleed, Include Artboard Fill, Full Document), Export to, Open Location after Export, Create Sub-folders by Scale or by Format (on by default), Export PDFs As (Single File writes the chosen artboards as the pages of one PDF named after the document, `pdfs: "single"`), and the format rows with the iOS and Android buttons, the format presets (☰) and Format Settings; under them, how many items are selected and how many files the export writes. File name patterns, the Layers and Selection sources and per-item folders stay available through `document.exportForScreens` (scripts and MCP). Tests: `dialogs::tests_screens` (layout, iOS/Android, Clear Selection, sub-folders, Single File) and `fileio::tests_screens::single_file_pdfs_hold_every_chosen_artboard`. - Save for Web: Preview in Browser writes each preview's page and images to a folder of its own inside `VectorCraft Save for Web` in the system's temporary folder. A later preview, from the same app or another one running, does not overwrite an earlier one. Each preview first removes the preview folders there that are an hour old or older and, on Linux, macOS and FreeBSD, belong to the current user; it leaves other files and folders alone. The folders a preview makes there are open to their owner only on those systems. When `VectorCraft Save for Web` is a link or is not a folder, or on those systems belongs to another user or can be written by other users, the preview goes to a new folder of its own in the temporary folder, which a later preview removes once it is an hour old (`file.saveForWeb.browser`). - Export for Screens, Save for Web (slice images, and files named after the document's title) and the Libraries panel name files with the characters Windows does not allow written as `-` and with a `-` after a Windows device name such as `CON`, on every platform (`document.exportForScreens`, `document.exportForWeb`, `library.create`).