From 5a5f1a099502c30208357e3db23275c109042717 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 14 Oct 2026 20:57:00 -0700 Subject: [PATCH] Properties: Edit Artboards renames the active artboard and opens Artboard Options With the Artboard tool, the Properties panel showed the active artboard's name as a label, and no control in the panel opened Artboard Options. The name is now a text field. Typing a new name and pressing Enter or clicking elsewhere renames the active artboard in one undo step through artboard.setProps, as the Artboards panel's rename does; pressing Escape or leaving the field empty keeps the old name. A click on the Artboard Options icon beside the field runs tool.options with tool "artboard", which opens Artboard Options for the active artboard, as a double-click on the Artboard tool's button does. Docs: docs/target-app-parity.md. --- crates/ui-egui/src/panels/properties.rs | 111 +++++++++++++++++++++++- docs/target-app-parity.md | 1 + 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index ebad30a0e..3aa66e384 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -200,7 +200,8 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { } } -/// Edit Artboards (the Artboard tool): the active artboard's name, position and size, its preset and +/// Edit Artboards (the Artboard tool): the active artboard's name, which can be edited to rename +/// it, and the Artboard Options icon; its position and size, its preset and /// orientation (#671) (with Scale Artwork with Artboard on, a new size scales its art, #602), Move /// and Scale Artwork with Artboard, New Artboard and Delete Artboard, and Quick Actions: Rearrange All (#681) and /// Exit back to the Selection tool (#530). @@ -212,8 +213,20 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let scale_art = super::artboards::scale_art(app); let move_art = super::artboards::move_art(app); if let Some(ab) = app.session.active().and_then(|d| d.doc.artboards.get(i)).cloned() { - // Its name as it is (names are never translated). - ui.label(egui::RichText::new(&ab.name).size(13.0).color(Tokens::get(ui.ctx()).text)); + // The field shows the artboard's name as it is (names are never translated). Typing a new + // name and pressing Enter or clicking elsewhere renames the artboard as the Artboards panel's + // rename does; pressing Escape or leaving the field empty keeps the old name. A click on + // the icon opens the artboard's Artboard Options, as double-clicking the Artboard tool's + // button does. + ui.horizontal(|ui| { + let w = (ui.available_width() - 26.0 - ui.spacing().item_spacing.x).max(80.0); + if let Some(name) = widgets::text_field(ui, ("ab", "name"), Some(&ab.name), w, 1).filter(|n| !n.is_empty()) { + app.run("artboard.setProps", json!({"index": i, "name": name})).ok(); + } + if widgets::icon_button(ui, "dc-artboard-options", tl!("Artboard Options"), false, 26.0).clicked() { + app.run("tool.options", json!({"tool": "artboard"})).ok(); + } + }); let fw = super::field_width(ui); egui::Grid::new("ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| { let r = ab.rect; @@ -822,6 +835,98 @@ mod tests { assert_eq!(app.session.tool_id(), "selection"); } + /// Typing a name into Edit Artboards' name field and pressing Enter renames the active artboard + /// alone, trimmed as the Artboards panel's rename trims it, with one command and one undo step; + /// a click elsewhere in the panel renames it too. Pressing Escape or leaving the field empty + /// keeps the name and runs no command. + #[test] + fn edit_artboards_renames_the_active_artboard() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + super::super::artboards::select(&mut app, 1); + app.session.doc_mut().unwrap().mark_saved(); + let before = app.session.doc().unwrap().doc.artboards.clone(); + let undo = app.session.doc().unwrap().history.undo.len(); + let ctx = egui::Context::default(); + let key = |key, modifiers| vec![Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }]; + // Click the field showing Artboard 2's name, select all of it, type `typed`, then send each + // frame's events of `end` → the number of commands run. + let rename = |app: &mut VectorcraftApp, typed: Vec, end: Vec>| { + frame(app, &ctx, vec![]); + let texts = frame(app, &ctx, vec![]); + let name = app.session.doc().unwrap().doc.artboards[1].name.clone(); + click(app, &ctx, &texts, &name); + frame(app, &ctx, key(egui::Key::A, egui::Modifiers::COMMAND)); + let commands = app.run_count; + frame(app, &ctx, typed); + for events in end { + frame(app, &ctx, events); + } + frame(app, &ctx, vec![]); + app.run_count.wrapping_sub(commands) + }; + let enter = || vec![key(egui::Key::Enter, egui::Modifiers::NONE)]; + let text = |s: &str| vec![Event::Text(s.into())]; + assert_eq!(rename(&mut app, text("Cover"), vec![key(egui::Key::Escape, egui::Modifiers::NONE)]), 0, "Escape"); + assert_eq!(rename(&mut app, key(egui::Key::Backspace, egui::Modifiers::NONE), enter()), 0, "an empty field"); + assert_eq!(app.session.doc().unwrap().doc.artboards, before); + assert_eq!(rename(&mut app, text(" Cover "), enter()), 1); + let mut renamed = before.clone(); + renamed[1].name = "Cover".into(); + let st = app.session.doc().unwrap(); + assert_eq!(st.doc.artboards, renamed, "Artboard 2 alone is renamed"); + assert_eq!(st.history.undo.len(), undo + 1, "one undo step"); + app.run("edit.undo", json!({})).unwrap(); + let st = app.session.doc().unwrap(); + assert_eq!(st.doc.artboards, before); + assert!(!st.is_dirty()); + // The click elsewhere is a press and release on the panel's "Artboard" heading, outside + // the field. + let at = frame(&mut app, &ctx, vec![]).iter().find(|(t, _)| t == "Artboard").map(|(_, r)| r.center()).unwrap(); + let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + assert_eq!(rename(&mut app, text("Back"), vec![vec![Event::PointerMoved(at), b(true)], vec![b(false)]]), 1, "a click elsewhere"); + renamed[1].name = "Back".into(); + assert_eq!(app.session.doc().unwrap().doc.artboards, renamed); + } + + /// A click on Edit Artboards' Artboard Options icon opens Artboard Options for the active + /// artboard, as double-clicking the Artboard tool's button does (`tool.options {tool: "artboard"}`). + #[test] + fn edit_artboards_opens_the_active_artboards_options() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + super::super::artboards::select(&mut app, 1); + app.run("tool.options", json!({"tool": "artboard"})).unwrap(); + let want = app.ui.dialog.take().unwrap().fields; + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let texts = frame(&mut app, &ctx, vec![]); + // The icon is the rightmost clickable widget on the name's line. + let name = texts.iter().find(|(t, _)| t == "Artboard 2").map(|(_, r)| *r).unwrap(); + let icon = ctx.viewport(|vp| { + vp.prev_pass + .widgets + .layers() + .flat_map(|(_, w)| w.iter()) + .filter(|w| w.sense.senses_click() && w.rect.y_range().contains(name.center().y)) + .map(|w| w.rect) + .max_by(|a, b| a.left().total_cmp(&b.left())) + .unwrap() + .center() + }); + let commands = app.run_count; + let press = |pressed| Event::PointerButton { pos: icon, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(&mut app, &ctx, vec![Event::PointerMoved(icon), press(true)]); + frame(&mut app, &ctx, vec![press(false)]); + assert_eq!(app.run_count, commands.wrapping_add(1), "one command"); + let d = app.ui.dialog.take().expect("Artboard Options"); + assert_eq!((d.kind.as_str(), d.f64("index", 9.0), d.str("name").as_str()), (crate::dialogs::artboard_options::KIND, 1.0, "Artboard 2")); + assert_eq!(d.fields, want, "the fields the tool's double-click shows"); + assert_eq!(app.session.tool_id(), "artboard"); + } + fn artboard_deletion_app(count: usize) -> VectorcraftApp { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.run("file.new", json!({"width": 200, "height": 100, "artboards": count})).unwrap(); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 558c7e141..4e186c1ec 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -394,6 +394,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Rulers (#1212):** drag the intersection to preview and set a document zero point, or double-click it to reset to the active artboard's top-left. Transform, Properties and Control X/Y fields measure from it and convert entered positions back to absolute command coordinates. The origin survives every native format version and compression; geometry, guides and patterns stay in place. Global/artboard mode switching and video rulers remain separate. Public reference: [Use rulers](https://helpx.adobe.com/ca/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html). - **Document Setup modal handoff:** Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool. Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`. +- **Edit Artboards in Properties:** the active artboard's name is a field. Typing a new name and pressing Enter or clicking elsewhere renames that artboard in one undo step, as the Artboards panel's rename does (`artboard.setProps {index, name}`); pressing Escape or leaving the field empty keeps the old name. A click on the Artboard Options icon beside the field opens Artboard Options for the active artboard, as double-clicking the Artboard tool's button does (`tool.options {tool: "artboard"}`). Pointer and key tests in `panels/properties.rs` cover both. Public references: [Resize artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/resize-artboards.html) and [Create and add new artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/add-new-artboards.html). - **Artboard Escape routing:** Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests_distortkeys.rs`. - **Artboard deletion keys:** Delete/Backspace delete selected boards in one Undo step, retaining the original first board in the current order when all are selected, as in the public [Delete artboards guide](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html). The last board and active drags stay protected; neither key clears selected artwork. Headless shortcut regressions in `tests_distortkeys.rs` cover reordered boards, artwork, saved state and Undo/Redo. - **Linux RPM packaging:** 64-bit runtime capabilities replace Fedora-specific required package names (#1275); glibc 2.35, libm and libgcc_s are declared alongside dynamically loaded xkbcommon and Vulkan/EGL. Generated x86_64/aarch64 metadata and isolated synthetic-provider resolution checked; real-distro install/start-up QA remains pending.