Fix #530: Edit Artboards adds, deletes and leaves artboards from Properties

The Properties panel's Edit Artboards turned on the Artboard tool but the
panel went on showing the document's sections, with no way there to add
an artboard (only dragging on the canvas, which the hint bar names, or
the Artboards panel's button). While the Artboard tool is on, the panel
now shows the active artboard: its name and X, Y, W and H (editable,
artboard.setProps), New Artboard (which becomes the active one), Delete
Artboard (with more than one), and Exit back to the Selection tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-09 02:29:26 +07:00
parent 857273001b
commit 1d013959cb
8 changed files with 116 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -1492,6 +1492,8 @@
New Art Brush from Selection Nuevo pincel de arte a partir de la selección
New Art Has Basic Appearance La nueva ilustración tiene apariencia básica
New Artboard Nueva mesa de trabajo
Artboard Mesa de trabajo
Exit Salir
New Bristle Brush Nuevo pincel de cerdas
New Brush… Nuevo pincel…
New Calligraphic Brush Nuevo pincel caligráfico

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1492,6 +1492,8 @@
New Art Brush from Selection Nuovo pennello artistico dalla selezione
New Art Has Basic Appearance La nuova grafica ha aspetto di base
New Artboard Nuova tavola da disegno
Artboard Tavola da disegno
Exit Esci
New Bristle Brush Nuovo pennello a setole
New Brush… Nuovo pennello…
New Calligraphic Brush Nuovo pennello calligrafico

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1635,6 +1635,8 @@
New Art Brush from Selection 選択範囲から新規アートブラシ
New Art Has Basic Appearance 新規アートに基本アピアランスを使用
New Artboard 新規アートボード
Artboard アートボード
Exit 終了
New Bristle Brush 新規剛毛ブラシ
New Brush… 新規ブラシ…
New Calligraphic Brush 新規絵筆ブラシ

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1469,6 +1469,7 @@
New Art Brush from Selection 从选区新建艺术画笔
New Art Has Basic Appearance 新建图稿使用基本外观
New Artboard 新建画板
Exit 退出
New Bristle Brush 新建鬃毛画笔
New Brush… 新建画笔…
New Calligraphic Brush 新建书法画笔

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -1509,6 +1509,8 @@
New Art Brush from Selection 從選取範圍新增藝術筆刷
New Art Has Basic Appearance 新圖稿具有基本外觀
New Artboard 新增工作區域
Artboard 工作區域
Exit 結束
New Bristle Brush 新增鬃毛筆刷
New Brush… 新增筆刷…
New Calligraphic Brush 新增書法筆刷

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -13,13 +13,13 @@ use crate::widgets::{self, menu_item};
use crate::{VectorcraftApp, icons};
/// The active artboard (of `n`).
fn selected(app: &VectorcraftApp, n: usize) -> usize {
pub(crate) fn selected(app: &VectorcraftApp, n: usize) -> usize {
app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1))
}
/// Make artboard `i` the active one, leaving the view where it is: the navigator's and, while it
/// is the tool, the Artboard tool's.
fn select(app: &mut VectorcraftApp, i: usize) {
pub(crate) fn select(app: &mut VectorcraftApp, i: usize) {
if let Some(v) = app.view_mut() {
v.artboard = i;
}

View File

@ -18,7 +18,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let n_sel = st.selection.len();
let anchors = crate::chrome::anchor_controls(app);
let first = first_selected(app);
// Edit Artboards (the Artboard tool): the active artboard, whatever is selected.
let editing_artboards = app.session.tool_id() == "artboard";
let label = match (&first, n_sel) {
_ if editing_artboards => tl!("Artboard").to_string(),
(None, _) => tl!("Document").to_string(),
(_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"),
(Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(),
@ -26,6 +29,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
};
ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim));
ui.add_space(4.0);
if editing_artboards {
artboard_sections(app, ui);
return;
}
if first.is_none() {
document_sections(app, ui);
return;
@ -188,6 +195,48 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
}
}
/// Edit Artboards (the Artboard tool): the active artboard's name, position and size, New Artboard
/// and Delete Artboard, and Exit back to the Selection tool (#530).
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
let units = app.session.general_unit();
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let i = super::artboards::selected(app, n);
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));
let fw = ((ui.available_width() - 50.0) / 2.0).clamp(60.0, 110.0);
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;
for (row, [(l1, k1, v1), (l2, k2, v2)]) in
[[("X:", "x", r.x0), ("W:", "width", r.width())], [("Y:", "y", r.y0), ("H:", "height", r.height())]].into_iter().enumerate()
{
for (label, key, v) in [(l1, k1, v1), (l2, k2, v2)] {
dim_label(ui, label);
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v), units, fw) {
app.run("artboard.setProps", json!({"index": i, key: v})).ok();
}
}
ui.end_row();
}
});
}
ui.add_space(4.0);
let w = (ui.available_width() - 6.0) / 2.0;
ui.horizontal(|ui| {
if widgets::flat_button(ui, tl!("New Artboard"), w).clicked() && app.run("artboard.new", json!({})).is_ok() {
super::artboards::select(app, n);
}
if ui.add_enabled_ui(n > 1, |ui| widgets::flat_button(ui, tl!("Delete Artboard"), w)).inner.clicked() {
app.run("artboard.delete", json!({"index": i})).ok();
}
});
divider(ui);
section_header(ui, tl!("Quick Actions"));
if widgets::flat_button(ui, tl!("Exit"), w).clicked() {
app.select_tool("selection");
}
}
fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
let units = app.session.general_unit();
section_header(ui, tl!("Document"));
@ -416,3 +465,58 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
}
});
}
#[cfg(test)]
mod tests {
use egui::{Event, PointerButton, Pos2, Rect, vec2};
use vectorcraft_engine::Session;
use super::*;
/// One frame of the panel with `events` → the texts painted, with their rects.
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
out.textures_delta.clear();
out.shapes
.iter()
.filter_map(|s| {
if let egui::Shape::Text(t) = &s.shape {
Some((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size())))
} else {
None
}
})
.collect()
}
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)], label: &str) {
let at = texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"));
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, ctx, vec![Event::PointerMoved(at), b(true)]);
frame(app, ctx, vec![b(false)]);
}
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
#[test]
fn edit_artboards_adds_artboards_and_exits() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
let ctx = egui::Context::default();
let texts = frame(&mut app, &ctx, vec![]);
click(&mut app, &ctx, &texts, "Edit Artboards");
assert_eq!(app.session.tool_id(), "artboard");
let texts = frame(&mut app, &ctx, vec![]);
assert!(texts.iter().any(|(t, _)| t == "Artboard 1") && texts.iter().any(|(t, _)| t == "200 pt"), "{texts:?}");
click(&mut app, &ctx, &texts, "New Artboard");
let boards = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.len();
assert_eq!(boards(&app), 2);
let texts = frame(&mut app, &ctx, vec![]);
assert!(texts.iter().any(|(t, _)| t == "Artboard 2"), "the new artboard is the active one: {texts:?}");
click(&mut app, &ctx, &texts, "Delete Artboard");
assert_eq!(boards(&app), 1);
let texts = frame(&mut app, &ctx, vec![]);
click(&mut app, &ctx, &texts, "Exit");
assert_eq!(app.session.tool_id(), "selection");
}
}