From 9334dfd8d78fee0ea6cf37f77bb562aad8eb036f Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Sat, 14 Nov 2026 20:09:22 +0900 Subject: [PATCH 1/4] Fix #7275: Rename artboards in Export for Screens --- crates/ui-egui/src/dialogs.rs | 3 + .../ui-egui/src/dialogs/export_for_screens.rs | 137 ++++++++++++++---- crates/ui-egui/src/dialogs/tests_screens.rs | 64 ++++++++ crates/ui-egui/src/shortcuts.rs | 5 +- docs/roadmap.md | 5 +- 5 files changed, 184 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5afad21b0d..b6a65e2b51 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -156,6 +156,9 @@ pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES, pub use export_as::{export_selection, open as open_export_as}; pub(crate) use export_for_screens::drop_unread_screen_settings; pub use export_for_screens::open as open_export_for_screens; +pub(crate) fn export_artboard_name_editing(d: &Dialog) -> bool { + d.kind == export_for_screens::KIND && d.fields.contains_key(export_for_screens::RENAME_ARTBOARD) +} pub(crate) use export_for_screens::{ KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats, import_presets as screen_import_presets, open_assets as open_export_for_screens_assets, preset_list as screen_preset_list, diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 54c98052bf..8957ca0fa9 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -391,6 +391,7 @@ 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); d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards"))); } ui.add_space(6.0); @@ -520,23 +521,32 @@ 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 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| { - tiles(ui, "efs-artboards", names.len(), list, |ui, i, r| { - match artboard_thumb(app, ui.ctx(), i) { - Some(tex) => { - let sz = tex.size_vec2(); - let s = (r.width() / sz.x).min(r.height() / sz.y).min(1.0); - let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)); - ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE); + tiles( + ui, + "efs-artboards", + names.len(), + list, + |ui, i, r| { + match artboard_thumb(app, ui.ctx(), i) { + Some(tex) => { + let sz = tex.size_vec2(); + let s = (r.width() / sz.x).min(r.height() / sz.y).min(1.0); + let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)); + ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE); + } + None => { + ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE); + } } - None => { - ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE); - } - } - let name = names.get(i).cloned().unwrap_or_default(); - (format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false)) - }) + let name = names.get(i).cloned().unwrap_or_default(); + (format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false)) + }, + Some(&mut |ui, i, rect, id, name| edit_artboard_name(ui, i, rect, id, name, &mut editing, &mut rename)), + ) .into_iter() .for_each(|i| { if let Some(b) = boards.get_mut(i) { @@ -544,6 +554,16 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l } }); }); + if d.str("select") != "full" + && let Some(edit) = editing + { + 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 boards != before { set_select(d, if boards.iter().all(|b| *b) { "all" } else { "range" }); d.fields.insert("range".into(), json!(range_text(&boards))); @@ -551,6 +571,50 @@ 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 edit_artboard_name( + ui: &mut egui::Ui, + index: usize, + rect: egui::Rect, + id: egui::Id, + name: &str, + editing: &mut Option<(usize, String, bool)>, + rename: &mut Option<(usize, String)>, +) -> bool { + if let Some((i, text, focus)) = editing.as_mut().filter(|(i, _, _)| *i == index) { + let field = id.with("field"); + let cancel = ui.input(|i| i.key_pressed(egui::Key::Escape)); + let enter = crate::shortcuts::pressed_anew(ui.ctx(), egui::Key::Enter); + if enter { + widgets::reject_dialog_commit(ui.ctx()); + } + let response = ui.put(rect, egui::TextEdit::singleline(text).id(field).font(egui::FontId::proportional(11.0)).desired_width(rect.width())); + if *focus { + 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())); + } + response.surrender_focus(); + *editing = None; + } + false + } else { + let response = ui.interact(rect, id, egui::Sense::click()); + if response.double_clicked() { + *editing = Some((index, name.into(), true)); + ui.ctx().request_repaint(); + } + // A double-click toggles twice, preserving the checked state while opening the editor. + response.clicked() + } +} + +type CaptionEdit<'a> = dyn FnMut(&mut egui::Ui, usize, egui::Rect, egui::Id, &str) -> bool + 'a; + /// `count` thumbnails in a grid (or, with `list`, a list) that scrolls: `paint` draws thumbnail `i` /// into its rect and gives its caption, its checkbox's name and whether it is checked → the /// thumbnails whose checkbox (or picture) was clicked. @@ -560,6 +624,7 @@ fn tiles( count: usize, list: bool, mut paint: impl FnMut(&mut egui::Ui, usize, egui::Rect) -> (String, String, bool), + mut caption_edit: Option<&mut CaptionEdit<'_>>, ) -> Vec { let t = Tokens::get(ui.ctx()); let mut toggled = vec![]; @@ -580,11 +645,22 @@ fn tiles( let label_at = if list { egui::pos2(pic.right() + 8.0, pic.center().y) } else { egui::pos2(pic.left(), pic.bottom() + 9.0) }; let galley = ui.painter().layout(caption, egui::FontId::proportional(11.0), t.text, if list { cell.x - side - 12.0 } else { side }); - ui.painter().galley(egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0), galley, t.text); + let label_pos = egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0); + let caption_clicked = if let Some(edit) = &mut caption_edit { + let rect = egui::Rect::from_min_max(egui::pos2(label_at.x, r.top().max(label_at.y - 9.0)), r.right_bottom()); + let clicked = edit(ui, i, rect, resp.id.with("name"), &name); + if !ui.memory(|m| m.has_focus(resp.id.with("name").with("field"))) { + ui.painter().galley(label_pos, galley, t.text); + } + clicked + } else { + ui.painter().galley(label_pos, galley, t.text); + false + }; // The checkbox sits on the thumbnail's lower left corner, as Illustrator's. let bx = egui::Rect::from_min_size(egui::pos2(pic.left() + 4.0, pic.bottom() - 18.0), egui::vec2(14.0, 14.0)); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(bx)); - if widgets::check_box(&mut child, &name, checked, true) || resp.clicked() { + if widgets::check_box(&mut child, &name, checked, true) || resp.clicked() || caption_clicked { toggled.push(i); } } @@ -621,18 +697,25 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) { // Asked for until its row is in view: the dialog's first frame only measures it. let scroll_to = d.fields.get("__scrollTo").and_then(Value::as_u64).filter(|id| shown.iter().any(|a| a.id == *id)); let mut in_view = scroll_to.is_none(); - let clicked = tiles(ui, "efs-assets", shown.len(), list, |ui, i, r| { - let Some(a) = shown.get(i) else { return (String::new(), String::new(), false) }; - crate::panels::asset_export::paint_thumb(ui, st, a, r); - if scroll_to == Some(a.id) { - if ui.clip_rect().contains_rect(r) { - in_view = true; - } else { - ui.scroll_to_rect(r, Some(egui::Align::Center)); + let clicked = tiles( + ui, + "efs-assets", + shown.len(), + list, + |ui, i, r| { + let Some(a) = shown.get(i) else { return (String::new(), String::new(), false) }; + crate::panels::asset_export::paint_thumb(ui, st, a, r); + if scroll_to == Some(a.id) { + if ui.clip_rect().contains_rect(r) { + in_view = true; + } else { + ui.scroll_to_rect(r, Some(egui::Align::Center)); + } } - } - (a.name.clone(), a.name.clone(), on.contains(&a.id)) - }); + (a.name.clone(), a.name.clone(), on.contains(&a.id)) + }, + None, + ); for i in clicked { if let Some(a) = shown.get(i) { if on.contains(&a.id) { diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index 1bd4f10afc..7baa4d5899 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1319,3 +1319,67 @@ fn the_dialog_builds_the_preset_list_again_only_when_the_presets_change() { let mine = list.as_array().unwrap().iter().find(|p| p["name"] == "Mine").unwrap(); 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 double_clicking_an_artboard_name_in_export_for_screens_renames_it() { + 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(), "double-clicking the artboard name must open an editable field"); + h.frame(&mut app, vec![egui::Event::Text("Renamed".into())]); + h.frame( + &mut app, + vec![egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }], + ); + h.settle(&mut app); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Renamed"); + assert_eq!(kind(&app), Some(export_for_screens::KIND)); + assert_eq!(field(&app, "boards"), before, "renaming must not change the checked artboards"); + assert!(log.borrow().is_empty(), "renaming must not export"); + // The real shortcut handler runs before the dialog body. Escape cancels only the name. + let out = h.settle(&mut app); + let at = screen_text_at(&out, "1 Renamed"); + 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); + h.frame(&mut app, vec![egui::Event::Text("Discard".into())]); + let key = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + h.time += 0.1; + let input = egui::RawInput { + events: vec![key], + time: Some(h.time), + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))), + ..Default::default() + }; + let mut out = h.ctx.run_ui(input, |ui| { + crate::shortcuts::handle(&mut app, ui.ctx()); + super::show(&mut app, ui.ctx()); + }); + out.textures_delta.clear(); + h.settle(&mut app); + assert_eq!(kind(&app), Some(export_for_screens::KIND)); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Renamed"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, old); + } +} diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index ea32f6c078..e79c5fe31f 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -226,7 +226,10 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { // Escape closes a dropdown list or popover open in the dialog (each closes itself on it) and // leaves the dialog open. let popup = app.ui.dialog.as_ref().is_some_and(|d| { - egui::Popup::is_any_open(ctx) || crate::dialogs::brush_options::tips_open(d) || crate::dialogs::new_document_popup_open(d) + egui::Popup::is_any_open(ctx) + || crate::dialogs::brush_options::tips_open(d) + || crate::dialogs::new_document_popup_open(d) + || crate::dialogs::export_artboard_name_editing(d) }); // Escape in Preferences' search field with text clears the field first. if !popup && pressed_anew(ctx, Key::Escape) && !crate::prefs_dialog::clear_search_on_escape(app, ctx) { diff --git a/docs/roadmap.md b/docs/roadmap.md index 1cf18c3ce3..98f4511756 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (#7275: rename artboards in Export for Screens; Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -159,7 +159,7 @@ Gujarati interface follow-up (2026-11-14): the catalog and zero/one plural rule | M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending; the Transform fields take one Tab each, the W/H link after them) | 4–6 | | M2 | Drawing tools + smart guides | ✅ mostly done (#3380 deletion creates independent selected path pieces and keeps bounds-fitted gradients in place; endpoint highlighting remains open; Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); a shape just drawn moves by its centre point and turns from the zone outside a corner with its tool still active (#5477); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537), including the existing Snap to Glyph and Alignment Guides toggles (#6164); no View submenu; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123) | 12–18 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper copies transformed character dimensions (#6404) and styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; Library panels' lists (swatch, Color Book, brush, symbol and graphic style libraries) fill a taller panel above its footer (#2992; scores and estimates unchanged); Graphic Styles list fills a taller panel above its footer (#2992; scores and estimates unchanged); Swatches list fills a taller panel above its footer (#1638; `the_list_fills_a_tall_panel_above_its_footer`; scores and estimates unchanged); | 4–8 | -| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) | +| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | Rename artboards in Export for Screens (#7275); Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) | | M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); opaque solid-fill knockout paths draw once in hard-edge/supersampled raster export, with analytical-AA compositing unchanged; file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (live compound shapes: Make, Alt-click a Shape Mode, member modes, Release, Expand (#4522); single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; the slider opens at the Auto-Simplify level; a Direct Selection part of a path simplifies on its own); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open); Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog | 3–6 | | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | Spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof and the custom dictionary and Ignore All commands and Auto Spell Check with its right-click suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type, and the Edit Custom Dictionary dialog (#4936; the Check Spelling dialog remains); Right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops (#6147); A line a soft return ends is justified as a wrapped line is (#6137); Indian-language pattern hyphenation preserves syllables in both composers (#4947); 🟡 Hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room; Fullwidth `!` / `?` keep an upright fallback in vertical type; Hyphenation… and Justification… per paragraph (#2310, #2755), Auto Leading also driving the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs (feature-valued alternates and unencoded glyphs applied in one Undo step while keeping Arabic joining, #5514), OpenType panel (including font-defined Stylistic Sets), Find Font (Document, Recent and System replacement sources, #5217), installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, show recently applied families first (#3515), filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas tatweels on the base letter's paint (`tests::harakat`); tab leaders, spell check, vertical type pending | 45–60 | @@ -207,6 +207,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Export for Screens: double-click an artboard name to rename it in the dialog (#7275). Scores and estimates unchanged. | | 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | | 2026-11-12 | minor | Gujarati (`gu`): 4,286-row machine-assisted catalog reviewed by a native speaker, zero/one singular plurals, desktop font fallback and optional Gujarati craft-fonts fallback; all 72 translation tests pass; a web font and about 2,600 untranslated messages remain; scores and estimates unchanged | | 2026-11-12 | minor | Explicit editing-data import keeps a single artboard beyond a capped 14,400 pt PDF page (#6865), with a warning; automatic mode stays unchanged. Scores and estimates unchanged. | From a05dca2e9d6209d54e3fef41769dd3b29fba7908 Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Sat, 14 Nov 2026 22:02:18 +0900 Subject: [PATCH 2/4] 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`). From 990a80aad8d7325d21d4b7ce91a0bb612af4653d Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Sun, 15 Nov 2026 02:37:40 +0900 Subject: [PATCH 3/4] Fix #7275: Preserve export selection on batched double-clicks --- .../ui-egui/src/dialogs/export_for_screens.rs | 8 ++++- crates/ui-egui/src/dialogs/tests_screens.rs | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 120b1164be..96d86d82ad 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -623,8 +623,14 @@ fn edit_artboard_name( if response.double_clicked() { *editing = Some((index, name.into(), true)); ui.ctx().request_repaint(); + // If both releases arrived this frame, the first click did not toggle the row. + // Otherwise, undo the toggle from the first click on this caption. + return !ui.input(|input| { + input.events.iter().filter(|event| { + matches!(event, egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } if rect.contains(*pos)) + }).nth(1).is_some() + }); } - // A double-click toggles twice, preserving the checked state while opening the editor. response.clicked() } } diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index a361d4290b..24cb203135 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1320,6 +1320,37 @@ 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 same_frame_artboard_double_click_keeps_export_selection() { + for view in ["grid", "list"] { + for checked in [true, false] { + let (mut app, _) = app(2, true); + open(&mut app); + set(&mut app, "view", json!(view)); + let mut h = ScreensFrame::new(); + let out = h.settle(&mut app); + let name = app.session.active().unwrap().doc.artboards[0].name.clone(); + let at = screen_text_at(&out, &format!("1 {name}")); + if !checked { + h.click(&mut app, at); + h.time += 1.0; + h.settle(&mut app); + } + let before = field(&app, "boards"); + h.frame(&mut app, vec![egui::Event::PointerMoved(at)]); + let events = [true, false, true, false] + .into_iter() + .map(|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }) + .collect(); + h.frame(&mut app, events); + h.settle(&mut app); + assert!(h.ctx.egui_wants_keyboard_input(), "{view}, checked={checked}"); + assert_eq!(field(&app, "boards"), before, "{view}, checked={checked}"); + } + } +} + /// #7275: a double-click on an artboard name enters rename without leaving Export for Screens. #[test] fn switching_export_tabs_commits_an_artboard_name() { From 09c6d5dcf1e4f218effaa160d613d492d6e72c7e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 13:23:35 -0800 Subject: [PATCH 4/4] Fix #7275: a typed artboard name follows its artboard The rename field in Export for Screens tracked the artboard by its index. If the artboards changed order while a name was typed (a command from the control channel or MCP), Enter renamed the artboard that had moved into that index. The field now tracks the artboard by its id and by the document it was opened in. When that artboard is deleted, or another document becomes active, while its name is typed, the field closes, no artboard is renamed, and Escape closes the dialog again. --- crates/ui-egui/src/dialogs.rs | 10 +- .../ui-egui/src/dialogs/export_for_screens.rs | 89 ++++++++++----- crates/ui-egui/src/dialogs/tests_screens.rs | 101 ++++++++++++++++++ crates/ui-egui/src/shortcuts.rs | 2 +- docs/roadmap.md | 3 +- docs/target-app-parity.md | 2 +- 6 files changed, 169 insertions(+), 38 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b6a65e2b51..cdbb6c69e8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -156,13 +156,11 @@ pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES, pub use export_as::{export_selection, open as open_export_as}; pub(crate) use export_for_screens::drop_unread_screen_settings; pub use export_for_screens::open as open_export_for_screens; -pub(crate) fn export_artboard_name_editing(d: &Dialog) -> bool { - d.kind == export_for_screens::KIND && d.fields.contains_key(export_for_screens::RENAME_ARTBOARD) -} pub(crate) use export_for_screens::{ - KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats, - import_presets as screen_import_presets, open_assets as open_export_for_screens_assets, preset_list as screen_preset_list, - saved_options as screen_saved_options, saved_rows as screen_saved_rows, + KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, artboard_name_editing as export_artboard_name_editing, + export_preset as screen_export_preset, formats as screen_formats, import_presets as screen_import_presets, + open_assets as open_export_for_screens_assets, preset_list as screen_preset_list, saved_options as screen_saved_options, + saved_rows as screen_saved_rows, }; pub(crate) use form::{humanize, number_list_text, plugin_color_button, read_number_list, unit_suffix}; pub use new_document::{create_without_dialog as new_document_without_dialog, open as open_new_document, preset_card}; diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 96d86d82ad..d44fa86e65 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -392,9 +392,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.set_width(LEFT_W); if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) { if i != tab - && let Some((index, text, _)) = take_artboard_edit(d) + && let Some((board, text, _)) = take_artboard_edit(app, d) { - rename_artboard(app, index, &text); + rename_artboard(app, board, &text); } d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards"))); } @@ -525,9 +525,10 @@ 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 = take_artboard_edit(d); + let mut editing = take_artboard_edit(app, 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(); + let ids: Vec = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.id).collect()).unwrap_or_default(); ui.add_enabled_ui(d.str("select") != "full", |ui| { tiles( ui, @@ -549,7 +550,7 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l let name = names.get(i).cloned().unwrap_or_default(); (format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false)) }, - Some(&mut |ui, i, rect, id, name| edit_artboard_name(ui, i, rect, id, name, &mut editing, &mut rename)), + Some(&mut |ui, i, rect, id, name| edit_artboard_name(ui, ids.get(i).copied(), rect, id, name, &mut editing, &mut rename)), ) .into_iter() .for_each(|i| { @@ -558,13 +559,15 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l } }); }); + // A field whose artboard was deleted while it was typed in is closed, so Escape reaches the dialog again. if d.str("select") != "full" - && let Some(edit) = editing + && let Some(uid) = app.session.active().map(|s| s.uid) + && let Some((board, text, focus)) = editing.filter(|(board, _, _)| ids.contains(board)) { - d.fields.insert(RENAME_ARTBOARD.into(), json!(edit)); + d.fields.insert(RENAME_ARTBOARD.into(), json!((uid, board, text, focus))); } - if let Some((i, name)) = rename { - rename_artboard(app, i, &name); + if let Some((board, name)) = rename { + rename_artboard(app, board, &name); } if boards != before { set_select(d, if boards.iter().all(|b| *b) { "all" } else { "range" }); @@ -574,33 +577,60 @@ 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"; +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()) +/// Whether an artboard name is being typed in Export for Screens: a field is open for an artboard +/// of the active document. +pub(crate) fn artboard_name_editing(app: &VectorcraftApp, d: &Dialog) -> bool { + d.kind == KIND + && d.fields + .get(RENAME_ARTBOARD) + .and_then(|value| serde_json::from_value::<(u64, u32, String, bool)>(value.clone()).ok()) + .zip(app.session.active()) + .is_some_and(|((uid, board, ..), s)| s.uid == uid && s.doc.artboards.iter().any(|a| a.id == board)) } -fn rename_artboard(app: &mut VectorcraftApp, index: usize, text: &str) { +/// The artboard being renamed (its id, so the name goes to the same artboard after the artboards +/// change order), the name as typed, and whether the field still has to take the focus. A field +/// opened in another document than the active one is closed. +fn take_artboard_edit(app: &VectorcraftApp, d: &mut Dialog) -> Option<(u32, String, bool)> { + let doc = app.session.active().map(|s| s.uid); + d.fields + .remove(RENAME_ARTBOARD) + .and_then(|value| serde_json::from_value::<(u64, u32, String, bool)>(value).ok()) + .filter(|(uid, ..)| Some(*uid) == doc) + .map(|(_, board, text, focus)| (board, text, focus)) +} + +/// The field of the artboard with id `board`, the same wherever its tile is drawn. +fn artboard_name_field(board: u32) -> egui::Id { + egui::Id::unique(("efs-artboard-name", board)) +} + +fn rename_artboard(app: &mut VectorcraftApp, board: u32, text: &str) { let name = text.trim(); + let Some(index) = app.session.active().and_then(|s| s.doc.artboards.iter().position(|a| a.id == board && a.name != name)) else { return }; 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); } } +/// The caption of the artboard with id `board`: its name field while it is renamed, else a target +/// for the double-click that opens it → whether the click toggles the artboard's checkbox, and +/// whether the field was drawn over the caption. fn edit_artboard_name( ui: &mut egui::Ui, - index: usize, + board: Option, rect: egui::Rect, id: egui::Id, name: &str, - editing: &mut Option<(usize, String, bool)>, - rename: &mut Option<(usize, String)>, -) -> bool { - if let Some((i, text, focus)) = editing.as_mut().filter(|(i, _, _)| *i == index) { - let field = id.with("field"); + editing: &mut Option<(u32, String, bool)>, + rename: &mut Option<(u32, String)>, +) -> (bool, bool) { + if let Some((b, text, focus)) = editing.as_mut().filter(|(b, _, _)| Some(*b) == board) { + let field = artboard_name_field(*b); let cancel = ui.input(|i| i.key_pressed(egui::Key::Escape)); let enter = crate::shortcuts::pressed_anew(ui.ctx(), egui::Key::Enter); if enter { @@ -612,30 +642,33 @@ fn edit_artboard_name( *focus = false; } else if cancel || enter || response.lost_focus() { if !cancel { - *rename = Some((*i, std::mem::take(text))); + *rename = Some((*b, std::mem::take(text))); } response.surrender_focus(); *editing = None; } - false + (false, true) } else { let response = ui.interact(rect, id, egui::Sense::click()); - if response.double_clicked() { - *editing = Some((index, name.into(), true)); + if let Some(board) = board + && response.double_clicked() + { + *editing = Some((board, name.into(), true)); ui.ctx().request_repaint(); // If both releases arrived this frame, the first click did not toggle the row. // Otherwise, undo the toggle from the first click on this caption. - return !ui.input(|input| { + let toggle = !ui.input(|input| { input.events.iter().filter(|event| { matches!(event, egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } if rect.contains(*pos)) }).nth(1).is_some() }); + return (toggle, false); } - response.clicked() + (response.clicked(), false) } } -type CaptionEdit<'a> = dyn FnMut(&mut egui::Ui, usize, egui::Rect, egui::Id, &str) -> bool + 'a; +type CaptionEdit<'a> = dyn FnMut(&mut egui::Ui, usize, egui::Rect, egui::Id, &str) -> (bool, bool) + 'a; /// `count` thumbnails in a grid (or, with `list`, a list) that scrolls: `paint` draws thumbnail `i` /// into its rect and gives its caption, its checkbox's name and whether it is checked → the @@ -670,8 +703,8 @@ fn tiles( let label_pos = egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0); let caption_clicked = if let Some(edit) = &mut caption_edit { let rect = egui::Rect::from_min_max(egui::pos2(label_at.x, r.top().max(label_at.y - 9.0)), r.right_bottom()); - let clicked = edit(ui, i, rect, resp.id.with("name"), &name); - if !ui.memory(|m| m.has_focus(resp.id.with("name").with("field"))) { + let (clicked, field) = edit(ui, i, rect, resp.id.with("name"), &name); + if !field { ui.painter().galley(label_pos, galley, t.text); } clicked diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index 24cb203135..e26c82de60 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1446,3 +1446,104 @@ fn double_clicking_an_artboard_name_in_export_for_screens_renames_it() { assert_eq!(app.session.active().unwrap().doc.artboards[0].name, old); } } + +/// #7275: the typed name goes to the artboard whose name was double-clicked, even when the +/// artboards change order (a command from the control channel or MCP) while it is typed. When that +/// artboard is deleted, the field closes and Escape closes the dialog. +#[test] +fn an_artboard_name_follows_its_artboard_while_it_is_typed() { + for view in ["grid", "list"] { + let (mut app, log) = app(3, true); + open(&mut app); + set(&mut app, "view", json!(view)); + let mut h = ScreensFrame::new(); + let names = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect::>(); + let rename = |h: &mut ScreensFrame, app: &mut VectorcraftApp, label: &str| { + let out = h.settle(app); + let at = screen_text_at(&out, label); + h.frame(app, vec![egui::Event::PointerMoved(at)]); + for pressed in [true, false, true, false] { + h.frame( + app, + vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }], + ); + } + h.settle(app); + assert!(h.ctx.egui_wants_keyboard_input(), "{view}: the double-click opens the name"); + }; + let enter = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + rename(&mut h, &mut app, "2 Artboard 2"); + app.run("artboard.reorder", json!({"index": 1, "to": 0})).unwrap(); + h.frame(&mut app, vec![egui::Event::Text("Hero".into())]); + h.frame(&mut app, vec![enter]); + h.settle(&mut app); + assert_eq!(names(&app), ["Hero", "Artboard 1", "Artboard 3"], "{view}"); + assert_eq!(kind(&app), Some(export_for_screens::KIND), "{view}"); + assert!(log.borrow().is_empty(), "{view}: renaming must not export"); + // The artboard is deleted while its name is typed: the field closes, no other artboard + // takes the name, and Escape closes the dialog again. + rename(&mut h, &mut app, "2 Artboard 1"); + h.frame(&mut app, vec![egui::Event::Text("Gone".into())]); + app.run("artboard.delete", json!({"indices": [1]})).unwrap(); + h.settle(&mut app); + assert!(!h.ctx.egui_wants_keyboard_input(), "{view}: the field of a deleted artboard closes"); + assert_eq!(names(&app), ["Hero", "Artboard 3"], "{view}"); + let escape = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + h.time += 0.1; + let input = egui::RawInput { + events: vec![escape], + time: Some(h.time), + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))), + ..Default::default() + }; + let mut out = h.ctx.run_ui(input, |ui| { + crate::shortcuts::handle(&mut app, ui.ctx()); + super::show(&mut app, ui.ctx()); + }); + out.textures_delta.clear(); + h.settle(&mut app); + assert_eq!(kind(&app), None, "{view}: Escape closes the dialog"); + assert_eq!(names(&app), ["Hero", "Artboard 3"], "{view}"); + assert!(log.borrow().is_empty(), "{view}: renaming must not export"); + } +} + +/// #7275: a name typed while another document becomes active (a command from the control channel +/// or MCP) renames no artboard in either document, and Escape closes the dialog. +#[test] +fn an_artboard_name_stays_with_its_document() { + let (mut app, log) = app(2, true); + app.run("file.new", json!({"width": 60, "height": 40, "artboards": 2})).unwrap(); + app.run("document.activate", json!({"index": 0})).unwrap(); + open(&mut app); + let mut h = ScreensFrame::new(); + let names = |app: &VectorcraftApp, i: usize| app.session.documents()[i].doc.artboards.iter().map(|a| a.name.clone()).collect::>(); + let out = h.settle(&mut app); + let at = screen_text_at(&out, "2 Artboard 2"); + 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(), "the double-click opens the name"); + h.frame(&mut app, vec![egui::Event::Text("Elsewhere".into())]); + app.run("document.activate", json!({"index": 1})).unwrap(); + let escape = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + h.time += 0.1; + let input = egui::RawInput { + events: vec![escape], + time: Some(h.time), + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))), + ..Default::default() + }; + let mut out = h.ctx.run_ui(input, |ui| { + crate::shortcuts::handle(&mut app, ui.ctx()); + super::show(&mut app, ui.ctx()); + }); + out.textures_delta.clear(); + h.settle(&mut app); + assert_eq!(kind(&app), None, "Escape closes the dialog"); + assert_eq!(names(&app, 0), ["Artboard 1", "Artboard 2"]); + assert_eq!(names(&app, 1), ["Artboard 1", "Artboard 2"]); + assert!(log.borrow().is_empty(), "renaming must not export"); +} diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index e79c5fe31f..d102c40d7e 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -229,7 +229,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { egui::Popup::is_any_open(ctx) || crate::dialogs::brush_options::tips_open(d) || crate::dialogs::new_document_popup_open(d) - || crate::dialogs::export_artboard_name_editing(d) + || crate::dialogs::export_artboard_name_editing(app, d) }); // Escape in Preferences' search field with text clears the field first. if !popup && pressed_anew(ctx, Key::Escape) && !crate::prefs_dialog::clear_search_on_escape(app, ctx) { diff --git a/docs/roadmap.md b/docs/roadmap.md index 98f4511756..1f89391864 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (#7275: rename artboards in Export for Screens; Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -207,7 +207,6 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| -| 2026-11-14 | minor | Export for Screens: double-click an artboard name to rename it in the dialog (#7275). Scores and estimates unchanged. | | 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | | 2026-11-12 | minor | Gujarati (`gu`): 4,286-row machine-assisted catalog reviewed by a native speaker, zero/one singular plurals, desktop font fallback and optional Gujarati craft-fonts fallback; all 72 translation tests pass; a web font and about 2,600 untranslated messages remain; scores and estimates unchanged | | 2026-11-12 | minor | Explicit editing-data import keeps a single artboard beyond a capped 14,400 pt PDF page (#6865), with a warning; automatic mode stays unchanged. Scores and estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ce2345f56d..b6f29053c8 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -799,7 +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 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. The typed name goes to the artboard that was double-clicked even when the artboards change order while it is typed; the field closes when its artboard is deleted or another document becomes active. Evidence: `dialogs::tests_screens::double_clicking_an_artboard_name_in_export_for_screens_renames_it`, `dialogs::tests_screens::switching_export_tabs_commits_an_artboard_name`, `dialogs::tests_screens::an_artboard_name_follows_its_artboard_while_it_is_typed` and `dialogs::tests_screens::an_artboard_name_stays_with_its_document`. - 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`).