From 9b9f0918f0131017f8505df7dd0a9cd8193505af Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 1 Nov 2026 09:54:36 +0700 Subject: [PATCH] Fix #4687: the Artboards panel reorders artboards by dragging a row --- crates/ui-egui/src/panels/artboards.rs | 20 ++++++++- crates/ui-egui/src/panels/artboards/tests.rs | 44 ++++++++++++++++++++ docs/ui-parity.md | 4 +- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index 695f63fd6..77023bacb 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -1,6 +1,6 @@ //! Artboards panel: numbered list with inline rename (double-click the name, or the panel menu's //! Rename; while several artboards are selected, the name typed goes to all of them, numbered), -//! move up / down, new +//! move up / down (or drag a row above or below another), new //! (Alt-click: Artboard Options for the new artboard first), delete; a row dragged onto New //! Artboard duplicates its artboard (as Duplicate Artboards does, with //! its art when the Artboard tool's Move/Copy Artwork with Artboard is on). The highlighted row is the active artboard, the one the status bar's navigator shows and @@ -405,6 +405,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let all = picked(app, abs.len()); let editing: Option = pstate(ui.ctx(), "ab-edit"); let mut row_clicked = false; + // A row dropped on another row: (from, to), the index it lands at. + let mut dropped = None; let list_rect = widgets::list_box(ui, |ui| { ui.set_min_height(110.0); ui.spacing_mut().item_spacing.y = 0.0; @@ -512,10 +514,26 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } else if resp.double_clicked() { start_rename(ui.ctx(), app, i); } + // Another row dragged over this one lands above or below it by the pointer's half, + // a line showing where. + if let Some(from) = resp.dnd_hover_payload::().map(|d| d.0).filter(|from| *from != i) { + let after = ui.input(|inp| inp.pointer.interact_pos()).is_some_and(|p| p.y > r.center().y); + let y = if after { r.bottom() } else { r.top() }; + ui.painter().line_segment([pos2(r.left(), y), pos2(r.right(), y)], egui::Stroke::new(2.0, t.accent)); + if resp.dnd_release_payload::().is_some() { + let to = i + usize::from(after); + dropped = Some((from, if from < to { to - 1 } else { to })); + } + } } }); scrolled.inner_rect }); + if let Some((from, to)) = dropped.filter(|(from, to)| from != to) + && app.run("artboard.reorder", json!({"index": from, "to": to})).is_ok() + { + select(app, to); + } // Keyboard: the panel takes it with a click on a row and gives it up when the pointer is // pressed outside the list or the panel is not drawn (`ab-keys`: the frame it last had it in). let pressed_outside = ui.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| !list_rect.contains(p))); diff --git a/crates/ui-egui/src/panels/artboards/tests.rs b/crates/ui-egui/src/panels/artboards/tests.rs index 15bab000b..edae3a32e 100644 --- a/crates/ui-egui/src/panels/artboards/tests.rs +++ b/crates/ui-egui/src/panels/artboards/tests.rs @@ -994,6 +994,50 @@ fn dragging_a_row_onto_new_artboard_duplicates_it() { assert_eq!(app.view().unwrap().artboard, 1); } +/// #4687: a row dragged above or below another row reorders the artboards there (a line shows +/// where it lands), in one undo step, and the moved artboard becomes the active one. +#[test] +fn dragging_a_row_onto_another_row_reorders_the_artboards() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 3})).unwrap(); + let ctx = egui::Context::default(); + let screen = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(240.0, 400.0))); + let frame = |app: &mut VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: screen, ..Default::default() }, |ui| show(app, ui)); + out.textures_delta.clear(); + out.shapes + }; + let shapes = frame(&mut app, vec![]); + let name_at = |label: &str| { + shapes + .iter() + .find_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos + vec2(4.0, 4.0)), + _ => None, + }) + .unwrap() + }; + let (first, last) = (name_at("Artboard 1"), name_at("Artboard 3")); + let names = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect::>(); + let button = |at, pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + // Artboard 1 dragged onto the lower half of Artboard 3's row lands below it. + let below = last + vec2(0.0, 8.0); + frame(&mut app, vec![egui::Event::PointerMoved(first), button(first, true)]); + frame(&mut app, vec![egui::Event::PointerMoved(first + vec2(0.0, 12.0))]); + let held = frame(&mut app, vec![egui::Event::PointerMoved(below)]); + let accent = Tokens::get(&ctx).accent; + assert!( + held.iter().any(|c| matches!(&c.shape, egui::Shape::LineSegment { stroke, .. } if stroke.color == accent)), + "a line shows where it lands" + ); + frame(&mut app, vec![button(below, false)]); + assert_eq!(names(&app), ["Artboard 2", "Artboard 3", "Artboard 1"]); + assert_eq!(app.view().unwrap().artboard, 2, "the moved artboard is active"); + // One undo step puts it back. + app.session.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(names(&app), ["Artboard 1", "Artboard 2", "Artboard 3"]); +} + /// A click in a row's lock column locks its artboard and shows the lock next to the name, a /// second click unlocks it, and each click is one undo step. Delete Artboard leaves a locked /// artboard. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index f9e9ce12e..3c618c46b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Path Precision Tools add-on: Expand Dashes into Paths, #4922) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Artboards panel: drag a row to reorder the artboards, #4687) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -228,6 +228,7 @@ tools, never checked side by side) isn't captured row by row. | Escape while typing ends the edit, keeps the type selected and switches to the Selection tool | done | #1802 (#1686, #1656) | | Selection tool: selected area type and type on a path show an in port and an out port (empty, an arrow where threaded, the red "+" of overflow); a click on either loads the text cursor; a click on a closed path, on empty canvas or a drag threads the text there, after the clicked frame from its out port, before it from its in port; Escape or another tool drops it; a double-click on a threaded port breaks the thread there | partial | #2572 (`text.thread.add`, `before` for the in port); the out port sits on the frame's right side above the bottom-right corner handle and the in port on its left side below the top-left one, so the handles can still be grabbed; a double-click on a threaded port breaks the thread (`text.thread.break`, [archived help](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html)); the loaded cursor borrows the I-beam | | Shift-click selects several artboards (Artboard tool, Artboards panel); they move, Alt-copy and delete together | done | `artboard.move`/`artboard.delete` `indices` (#1139); panel menu, trash and Properties share selected-set deletion; all-selected retains artboard 1 and a single board cannot be deleted, with pointer-event and Undo/Redo coverage | +| Artboards panel: drag a row above or below another to reorder the artboards (as Move Up / Move Down do) | done | #4687; a line shows where the row lands, `artboard.reorder` in one undo step, the moved artboard becomes active; public forum answers describe the drag | | Artboards panel › Delete Empty Artboards | done | `artboard.deleteEmpty`; one undo step, guides go too, locked artboards stay, and artboard 1 when every artboard is empty and none is locked; an artboard is empty when no object (hidden, locked and template art included; guides not) has visual bounds that meet or touch it, a group counting by its members and a clip group or clipping layer by its members inside the clip (our reading of "no artwork"; the pages give no finer rule); [spec](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html) | | Artboard Delete/Backspace delete selected boards while protecting artwork, the last board and active drags | done | Headless shortcut regressions in `tests/distortkeys.rs` cover retaining the original first board when all are selected, single-board protection, artwork, saved state and Undo/Redo; the navigator follows the tool’s surviving board through repeated deletion and history changes. | | Artboard navigator synchronizes the active tool selection before deletion | done | Actual status-bar Next → Delete/Backspace and Next → Properties Delete regressions in `chrome.rs` cover surviving board records, preserved artwork and Undo/Redo; other tools keep object selection and history. **Tab-switch follow-up:** document-tab changes align the Artboard tool with the displayed board. Actual A → B → A tab clicks followed by Properties Delete and controls for same-tab multiselection, Move/Scale Artwork options and other tools passed, along with historical full CI on `a0671777`. [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area). **Clean-tab close:** with the Artboard tool active, closing a clean active tab restores the surviving document's displayed board before Properties Delete. | @@ -393,6 +394,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Artboards panel: a row dragged above or below another reorders the artboards (#4687); scores and estimates unchanged | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Export All Open Documents as PDF or SVG (#4920); scores and estimates unchanged | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Expand Dashes into Paths (#4922); scores and estimates unchanged | | 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Randomize Opacity (#4925); scores and estimates unchanged |