Merge pull request #5060 from storytold/fix/artboard-panel-reorder-4687

Fix #4687: the Artboards panel reorders artboards by dragging a row
This commit is contained in:
BFlat
2026-11-01 10:32:11 +07:00
committed by GitHub
3 changed files with 66 additions and 2 deletions
+19 -1
View File
@@ -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<usize> = 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::<RowDrag>().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::<RowDrag>().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)));
@@ -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<egui::Event>| {
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::<Vec<_>>();
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.
+3 -1
View File
@@ -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 |