Control bar: the Artboard tool shows the active artboard's preset, name, X, Y, W and H

With the Artboard tool in use, the Control bar showed Move and Scale
Artwork with Artboard and then the object controls (Fill, Stroke,
Opacity and the rest). The reference app's help lists the Control
panel, with the Artboard tool, among the places to add, delete,
rename, resize and position artboards, set their orientation and
reference point, and open Artboard Options.

While the tool is in use, the Control bar now shows the active
artboard's controls in place of the object controls: Preset, Portrait
and Landscape, New Artboard, Delete Artboard, the Name field, Artboard
Options, Rearrange All Artboards, the Transform link, Move and Scale
Artwork with Artboard, and the reference point with X, Y, W and H and
the Constrain Width and Height Proportions link. Each control runs the
command its Properties or Transform panel counterpart runs, and the
reference point is the one the Transform panel uses. In an 800-point
window the controls up to the Transform link fit, the two artwork
options show as their boxes with their names in tooltips, and the link
opens the position and size in a popover, as it does for objects.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 15:14:42 +07:00
committed by bflatastic
parent d949c3d8b7
commit 0b6f3285ab
6 changed files with 394 additions and 26 deletions
+6
View File
@@ -266,6 +266,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
let first = sel.first().and_then(|id| st.doc.node(*id)).cloned();
let anchors = anchor_controls(app);
let label = match &first {
_ if app.session.tool_id() == "artboard" => tl!("Artboard"),
Some(_) if sel.len() == 1 && anchors != AnchorControls::None => {
tl!("Anchor Point")
}
@@ -282,6 +283,11 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
_ => tl!(crate::panels::appearance::object_label(app)),
};
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
// With the Artboard tool in use, the bar shows the active artboard's controls in place
// of the object controls.
if crate::panels::artboards::control_bar(app, ui) {
return;
}
if anchors != AnchorControls::None {
anchor_buttons(app, ui, anchors);
}
+352 -2
View File
@@ -14,17 +14,107 @@ use crate::widgets::{self, menu_item};
use crate::{VectorcraftApp, icons};
/// The Artboard tool's Move Artwork with Artboard and Scale Artwork with Artboard options (#602) as
/// check boxes: the Control bar and Properties show them while the tool is in use.
/// check boxes: the Control bar ([`control_bar`]) and Properties show them while the tool is in use.
pub(crate) fn art_options(app: &mut VectorcraftApp, ui: &mut Ui) {
art_option_boxes(app, ui, false);
}
/// [`art_options`], or with `compact` the boxes alone, each option's name in its tooltip.
fn art_option_boxes(app: &mut VectorcraftApp, ui: &mut Ui, compact: bool) {
let opts = app.session.tool_options();
for (key, label, default) in [("moveArt", tl!("Move Artwork with Artboard"), true), ("scaleArt", tl!("Scale Artwork with Artboard"), false)] {
let on = opts[key].as_bool().unwrap_or(default);
if widgets::check(ui, label, on, true) {
let toggled = if compact { widgets::check_tip(ui, "", on, true, label) } else { widgets::check(ui, label, on, true) };
if toggled {
app.run("tool.setOption", json!({ "key": key, "value": !on })).ok();
}
}
}
/// The room Move and Scale Artwork with Artboard need with their labels in the Control bar; with
/// less, the boxes show alone.
const ART_OPTIONS_WIDTH: f32 = 370.0;
/// The room the Control bar's inline reference point and X/Y/W/H fields for an artboard need; with
/// less, they show only in the Transform link's popover.
const INLINE_FIELDS_WIDTH: f32 = 480.0;
/// The Control bar while the Artboard tool is in use, in place of the object controls: the active
/// artboard's preset and orientation, New Artboard and Delete Artboard, its name, Artboard Options,
/// Rearrange All Artboards and the Transform link (the Transform panel in a popover), which all fit
/// in the narrowest window; then Move and Scale Artwork with Artboard (the boxes alone, with their
/// names in tooltips, when the bar is short of room) and, while the bar has room, the reference
/// point the Transform panel also uses with the artboard's X and Y, and its W and H with the
/// Constrain Width and Height Proportions link. Each control runs the command its Properties panel
/// or Transform panel counterpart runs. Returns false, and draws nothing, while another tool is in
/// use.
pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
if app.session.tool_id() != "artboard" {
return false;
}
let t = Tokens::get(ui.ctx());
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let i = selected(app, n);
let Some(ab) = app.session.active().and_then(|d| d.doc.artboards.get(i)).cloned() else { return true };
let r = ab.rect;
super::properties::artboard_size_row(app, ui, i, r.width(), r.height());
if widgets::icon_button(ui, "dc-new-item", tl!("New Artboard"), false, 24.0).clicked() && app.run("artboard.new", json!({})).is_ok() {
select(app, n);
}
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > 1, 24.0).clicked() {
delete_selected(app);
}
ui.separator();
// The field shows the name as it is (names are never translated). A new name is applied on Enter
// or a click elsewhere, and an empty field or Escape keeps the old one, as in the Properties panel.
ui.label(egui::RichText::new(tl!("Name:")).size(12.0).color(t.text));
if let Some(name) = widgets::text_field(ui, ("cb-ab", "name"), Some(&ab.name), 120.0, 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, 24.0).clicked() {
app.run("tool.options", json!({"tool": "artboard"})).ok();
}
if widgets::icon_button_enabled(ui, "dc-rearrange", tl!("Rearrange All Artboards"), false, n > 1, 24.0).clicked() {
open_rearrange(app);
}
ui.separator();
super::transform::link(app, ui);
ui.separator();
art_option_boxes(app, ui, ui.available_width() < ART_OPTIONS_WIDTH);
if ui.available_width() < INLINE_FIELDS_WIDTH {
return true;
}
ui.separator();
let units = app.session.general_unit();
let origin = app.ruler_origin();
let refi = super::transform::reference_index(ui);
super::transform::reference_locator(ui, refi);
let link = super::transform::proportional(app, ui);
let rp = vectorcraft_geom::reference_point(r, refi);
for (k, lbl, v) in [("x", "X:", rp.x - origin.x), ("y", "Y:", rp.y - origin.y), ("width", "W:", r.width()), ("height", "H:", r.height())] {
// The W/H link sits between W and H.
if k == "height" {
super::transform::constrain_link(app, ui);
}
widgets::field_label(ui, egui::RichText::new(tl!(lbl)).size(12.0).color(t.text_dim));
let set = if matches!(k, "width" | "height") {
widgets::size_field(ui, ("cb-ab", k), Some(v), units, 80.0)
} else {
widgets::num_field(ui, ("cb-ab", k), Some(v), units, 80.0)
};
if let Some(nv) = set {
let xywh = match k {
"x" => [Some(nv + origin.x), None, None, None],
"y" => [None, Some(nv + origin.y), None, None],
"width" => [None, None, Some(nv), None],
_ => [None, None, None, Some(nv)],
};
super::transform::set_artboard(app, i, r, refi, xywh, link);
}
}
true
}
/// Whether artboards resized while the Artboard tool is in use take their art along (its Scale
/// Artwork with Artboard option): the panels' sizes pass it to `artboard.setProps` as `scaleArt`.
pub(crate) fn scale_art(app: &VectorcraftApp) -> bool {
@@ -608,6 +698,266 @@ mod tests {
assert_eq!((d.artboards.len(), d.layers[0].children().unwrap().len()), (3, 2), "one undo step");
}
/// One headless frame of the Control bar, 2,000 points wide, with `events` → the texts painted,
/// with their rects.
fn bar_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<(String, egui::Rect)> {
bar_frame_in(app, ctx, events, 2000.0)
}
/// [`bar_frame`] in a window `width` points wide.
fn bar_frame_in(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>, width: f32) -> Vec<(String, egui::Rect)> {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 300.0));
let mut out =
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| crate::chrome::control_bar(app, ui));
out.textures_delta.clear();
out.shapes
.iter()
.filter_map(|c| match &c.shape {
egui::Shape::Text(t) => Some((t.galley.text().to_string(), egui::Rect::from_min_size(t.pos, t.galley.size()))),
_ => None,
})
.collect()
}
/// A context with the app's fonts, which the Control bar's label uses.
fn bar_context() -> egui::Context {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx
}
/// Two frames of the Control bar → the texts the second one painted.
fn bar_texts(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<(String, egui::Rect)> {
bar_frame(app, ctx, vec![]);
bar_frame(app, ctx, vec![])
}
/// Where `label` is painted in `texts`.
fn text_rect(texts: &[(String, egui::Rect)], label: &str) -> egui::Rect {
texts.iter().find(|(t, _)| t == label).map(|(_, r)| *r).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
}
/// A press and a release at `at` on the Control bar, a frame each.
fn bar_click(app: &mut VectorcraftApp, ctx: &egui::Context, at: egui::Pos2) {
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
bar_frame(app, ctx, vec![egui::Event::PointerMoved(at), button(true)]);
bar_frame(app, ctx, vec![button(false)]);
}
/// Click the Control bar's field at `at`, select all of it, type `typed` and press Enter.
fn bar_type(app: &mut VectorcraftApp, ctx: &egui::Context, at: egui::Pos2, typed: &str) {
let key = |key, modifiers| vec![egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }];
bar_click(app, ctx, at);
bar_frame(app, ctx, key(egui::Key::A, egui::Modifiers::COMMAND));
bar_frame(app, ctx, vec![egui::Event::Text(typed.into())]);
bar_frame(app, ctx, key(egui::Key::Enter, egui::Modifiers::NONE));
bar_frame(app, ctx, vec![]);
}
/// The Control bar's clickable widgets of the last frame that lie between x `from` and `to`,
/// left to right.
fn bar_buttons(ctx: &egui::Context, from: f32, to: f32) -> Vec<egui::Rect> {
let mut all: Vec<egui::Rect> = ctx.viewport(|vp| {
vp.prev_pass
.widgets
.layers()
.flat_map(|(_, w)| w.iter())
.filter(|w| w.sense.senses_click() && w.rect.left() >= from && w.rect.right() <= to)
.map(|w| w.rect)
.collect()
});
all.sort_by(|a, b| a.left().total_cmp(&b.left()));
all
}
/// The artboards' rectangles, the undo steps and the artboards' names.
fn boards(app: &VectorcraftApp) -> (Vec<vectorcraft_geom::Rect>, usize, Vec<String>) {
let st = app.session.active().unwrap();
(st.doc.artboards.iter().map(|a| a.rect).collect(), st.history.undo.len(), st.doc.artboards.iter().map(|a| a.name.clone()).collect())
}
/// With the Artboard tool in use, the Control bar shows the active artboard's controls in place
/// of the object controls: its preset, name, the Transform link, Move and Scale Artwork with
/// Artboard, X, Y, W and H, left to right, and in a narrow window the link without the fields.
/// Another tool shows the object controls.
#[test]
fn the_control_bar_shows_the_artboard_tools_controls() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100, "units": "Points"})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
let ctx = bar_context();
let has = |texts: &[(String, egui::Rect)], s: &str| texts.iter().any(|(t, _)| t == s);
let texts = bar_texts(&mut app, &ctx);
assert!(has(&texts, "Opacity:") && !has(&texts, "Name:") && !has(&texts, "Preset:"), "{texts:?}");
app.select_tool("artboard");
let texts = bar_texts(&mut app, &ctx);
let order = [
"Artboard",
"Preset:",
"Name:",
"Artboard 1",
"Transform",
"Move Artwork with Artboard",
"Scale Artwork with Artboard",
"X:",
"Y:",
"W:",
"200 pt",
"H:",
];
let lefts: Vec<f32> = order.iter().map(|s| text_rect(&texts, s).left()).collect();
assert!(lefts.windows(2).all(|w| w[0] < w[1]), "{order:?} at {lefts:?}");
for s in ["Opacity:", "Stroke:"] {
assert!(!has(&texts, s), "{s}: {texts:?}");
}
// In a window that leaves 300 points right of Scale Artwork with Artboard, the inline fields
// go (X: would start inside the window).
let width = text_rect(&texts, "Scale Artwork with Artboard").right() + 300.0;
bar_frame_in(&mut app, &ctx, vec![], width);
let texts = bar_frame_in(&mut app, &ctx, vec![], width);
assert!(has(&texts, "Scale Artwork with Artboard") && !has(&texts, "X:") && !has(&texts, "W:"), "{texts:?}");
// In the app's narrowest window, 800 points wide, the name and the Transform link still fit,
// and a click on the link shows the artboard's X, Y, W and H in the Transform panel's popover.
let narrow = |app: &mut VectorcraftApp, events| bar_frame_in(app, &ctx, events, 800.0);
narrow(&mut app, vec![]);
let texts = narrow(&mut app, vec![]);
let link = text_rect(&texts, "Transform");
assert!(link.right() < 790.0 && text_rect(&texts, "Artboard 1").right() < link.left(), "{texts:?}");
// Move and Scale Artwork with Artboard show as their boxes alone, inside the window, and the
// second one turns Scale Artwork with Artboard on.
assert!(!has(&texts, "Move Artwork with Artboard"), "{texts:?}");
let boxes = bar_buttons(&ctx, link.right(), 790.0);
assert_eq!(boxes.len(), 2, "{boxes:?}");
let scale = |app: &VectorcraftApp| app.session.tool_options()["scaleArt"].as_bool();
assert_eq!(scale(&app), Some(false));
let press = |pressed| egui::Event::PointerButton {
pos: boxes[1].center(),
button: egui::PointerButton::Primary,
pressed,
modifiers: Default::default(),
};
narrow(&mut app, vec![egui::Event::PointerMoved(boxes[1].center()), press(true)]);
narrow(&mut app, vec![press(false)]);
assert_eq!(scale(&app), Some(true));
narrow(&mut app, vec![]);
let button =
|pressed| egui::Event::PointerButton { pos: link.center(), button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
narrow(&mut app, vec![egui::Event::PointerMoved(link.center()), button(true)]);
narrow(&mut app, vec![button(false)]);
let texts = narrow(&mut app, vec![]);
assert!(has(&texts, "W:") && has(&texts, "200 pt"), "the popover: {texts:?}");
}
/// The Control bar's W resizes the active artboard about the reference point in one undo step:
/// about its center at first, and from its top-left corner once the locator's top-left point is
/// chosen.
#[test]
fn the_control_bar_resizes_the_active_artboard_about_the_reference_point() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100, "units": "Points", "artboards": 2})).unwrap();
app.select_tool("artboard");
select(&mut app, 1);
let ctx = bar_context();
let (before, undo, _) = boards(&app);
let w = text_rect(&bar_texts(&mut app, &ctx), "W:");
bar_type(&mut app, &ctx, egui::pos2(w.right() + 40.0, w.center().y), "300");
let b = before[1];
let mut want = before.clone();
want[1] = vectorcraft_geom::Rect::new(b.x0 - 50.0, b.y0, b.x1 + 50.0, b.y1);
assert_eq!(boards(&app).0, want, "Artboard 2 widens about its center");
assert_eq!(boards(&app).1, undo + 1, "one undo step");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(boards(&app).0, before);
// The locator's points are the 9-point-square buttons; the top-left one is drawn first.
let texts = bar_texts(&mut app, &ctx);
let points: Vec<egui::Rect> = bar_buttons(&ctx, 0.0, text_rect(&texts, "X:").left())
.into_iter()
.filter(|r| (r.width() - 9.0).abs() < 0.5 && (r.height() - 9.0).abs() < 0.5)
.collect();
let top_left = points.iter().min_by(|a, b| (a.top(), a.left()).partial_cmp(&(b.top(), b.left())).unwrap()).unwrap();
bar_click(&mut app, &ctx, top_left.center());
assert_eq!(ctx.data(|d| d.get_temp::<usize>(egui::Id::new("refpt"))), Some(0), "the shared reference point is the top-left one");
let w = text_rect(&bar_texts(&mut app, &ctx), "W:");
bar_type(&mut app, &ctx, egui::pos2(w.right() + 40.0, w.center().y), "300");
want[1] = vectorcraft_geom::Rect::new(b.x0, b.y0, b.x0 + 300.0, b.y1);
assert_eq!(boards(&app).0, want, "Artboard 2 widens from its left edge");
}
/// The Control bar's Portrait and Landscape buttons swap the active artboard's width and height,
/// its top-left corner staying put, in one undo step each.
#[test]
fn the_control_bar_orientation_swaps_width_and_height() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100, "units": "Points"})).unwrap();
app.select_tool("artboard");
let ctx = bar_context();
let (_, undo, _) = boards(&app);
// Between "Preset:" and "Name:", the buttons that take a click: the preset menu, Portrait,
// Landscape and New Artboard (Delete Artboard is disabled with one artboard).
let buttons = |app: &mut VectorcraftApp| {
let texts = bar_texts(app, &ctx);
bar_buttons(&ctx, text_rect(&texts, "Preset:").right(), text_rect(&texts, "Name:").left())
};
let b = buttons(&mut app);
bar_click(&mut app, &ctx, b[b.len() - 3].center());
assert_eq!((boards(&app).0, boards(&app).1), (vec![vectorcraft_geom::Rect::new(0.0, 0.0, 100.0, 200.0)], undo + 1), "Portrait");
let b = buttons(&mut app);
bar_click(&mut app, &ctx, b[b.len() - 2].center());
assert_eq!((boards(&app).0, boards(&app).1), (vec![vectorcraft_geom::Rect::new(0.0, 0.0, 200.0, 100.0)], undo + 2), "Landscape");
}
/// Typing a name into the Control bar's Name field and pressing Enter renames the active
/// artboard alone, in one undo step.
#[test]
fn the_control_bar_name_field_renames_the_active_artboard() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
app.select_tool("artboard");
select(&mut app, 1);
let ctx = bar_context();
let (_, undo, _) = boards(&app);
let name = text_rect(&bar_texts(&mut app, &ctx), "Artboard 2");
bar_type(&mut app, &ctx, name.center(), "Cover");
assert_eq!((boards(&app).2, boards(&app).1), (vec!["Artboard 1".to_string(), "Cover".to_string()], undo + 1));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(boards(&app).2, ["Artboard 1", "Artboard 2"]);
}
/// The Control bar's New Artboard adds an artboard and makes it the active one, Artboard Options
/// opens the active artboard's options, Rearrange All Artboards opens its dialog, and Delete
/// Artboard deletes the selected artboard.
#[test]
fn the_control_bar_adds_deletes_and_opens_artboard_dialogs() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
app.select_tool("artboard");
let ctx = bar_context();
let texts = bar_texts(&mut app, &ctx);
// With one artboard Delete Artboard is disabled, so New Artboard is the last button before
// "Name:"; with two, Delete Artboard is.
let (preset, name) = (text_rect(&texts, "Preset:").right(), text_rect(&texts, "Name:").left());
let b = bar_buttons(&ctx, preset, name);
bar_click(&mut app, &ctx, b[b.len() - 1].center());
assert_eq!((boards(&app).2.len(), selected(&app, 2)), (2, 1), "New Artboard adds Artboard 2 and makes it active");
// Artboard Options and Rearrange All Artboards: the two buttons between the name and the
// Transform link.
let texts = bar_texts(&mut app, &ctx);
let between =
|texts: &[(String, egui::Rect)]| bar_buttons(&ctx, text_rect(texts, "Artboard 2").right(), text_rect(texts, "Transform").left());
let options = between(&texts)[0];
bar_click(&mut app, &ctx, options.center());
let d = app.ui.dialog.take().unwrap();
assert_eq!((d.kind.as_str(), d.f64("index", 9.0)), ("artboardOptions", 1.0));
let texts = bar_texts(&mut app, &ctx);
let rearrange = between(&texts)[1];
bar_click(&mut app, &ctx, rearrange.center());
assert_eq!(app.ui.dialog.take().map(|d| d.kind), Some(crate::dialogs::rearrange_artboards::KIND.to_string()));
bar_texts(&mut app, &ctx);
let b = bar_buttons(&ctx, preset, name);
bar_click(&mut app, &ctx, b[b.len() - 1].center());
assert_eq!(boards(&app).2, ["Artboard 1"], "Delete Artboard deletes Artboard 2");
}
/// Choosing the Artboard tool starts it on the active artboard, the one the panel highlights:
/// Artboard Options shows that artboard, the pointer moving over another artboard leaves it
/// active, and Delete removes it. Shift+O while the tool is in use keeps the artboards it has
+4 -4
View File
@@ -279,10 +279,10 @@ fn rearrange_button(app: &mut VectorcraftApp, ui: &mut Ui, artboards: usize, w:
}
/// Artboard `i`'s size preset (New Document's saved and built-in presets, by category, either way
/// round; Custom when its `w` × `h` points match none) and its orientation. A preset sizes it in its
/// current orientation; the other orientation swaps its width and height. Its top-left corner stays
/// put either way.
fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize, w: f64, h: f64) {
/// round; Custom when its `w` × `h` points match none) and its orientation, here and in the Control
/// bar. A preset sizes it in its current orientation; the other orientation swaps its width and
/// height. Its top-left corner stays put either way.
pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize, w: f64, h: f64) {
let landscape = w > h;
// A preset `pw` × `ph` turned to the artboard's orientation.
let turned = |pw: f64, ph: f64| if (pw > ph) == landscape || pw == ph { (pw, ph) } else { (ph, pw) };
+29 -13
View File
@@ -50,26 +50,42 @@ pub fn artboard_rect(r: Rect, refi: usize, x: Option<f64>, y: Option<f64>, w: Op
Rect::new(x0, y0, x0 + nw, y0 + nh)
}
/// The reference point the Transform panel and the Control bar share (0 to 8, the center by default).
pub fn reference_index(ui: &Ui) -> usize {
ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4)
}
/// The shared reference point's locator; a click on it chooses another point.
pub fn reference_locator(ui: &mut Ui, refi: usize) {
if let Some(i) = widgets::reference_point(ui, refi) {
ui.data_mut(|d| d.insert_temp(egui::Id::new("refpt"), i));
}
}
/// Set artboard `index` (now at `r`) to a new X, Y, W or H from reference point `refi`
/// ([`artboard_rect`]) with `artboard.setProps`, passing the Artboard tool's Move and Scale Artwork
/// with Artboard options as `moveArt` and `scaleArt`; an error is shown in the status bar.
pub fn set_artboard(app: &mut VectorcraftApp, index: usize, r: Rect, refi: usize, xywh: [Option<f64>; 4], link: bool) {
let [x, y, w, h] = xywh;
let n = artboard_rect(r, refi, x, y, w, h, link);
let scale_art = crate::panels::artboards::scale_art(app);
let move_art = crate::panels::artboards::move_art(app);
let p = json!({"index": index, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height(), "scaleArt": scale_art, "moveArt": move_art});
if let Err(e) = app.run("artboard.setProps", p) {
app.status(e);
}
}
/// The Artboard tool's view of the panel: the active artboard's position and size.
fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect) {
let units = app.session.general_unit();
let origin = app.ruler_origin();
let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4);
let refi = reference_index(ui);
let link = proportional(app, ui);
let rp = vectorcraft_geom::reference_point(r, refi);
let set = |app: &mut VectorcraftApp, x, y, w, h| {
let n = artboard_rect(r, refi, x, y, w, h, link);
let scale_art = crate::panels::artboards::scale_art(app);
let move_art = crate::panels::artboards::move_art(app);
let p = json!({"index": index, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height(), "scaleArt": scale_art, "moveArt": move_art});
if let Err(e) = app.run("artboard.setProps", p) {
app.status(e);
}
};
let set = |app: &mut VectorcraftApp, x, y, w, h| set_artboard(app, index, r, refi, [x, y, w, h], link);
ui.horizontal(|ui| {
if let Some(i) = widgets::reference_point(ui, refi) {
ui.data_mut(|d| d.insert_temp(egui::Id::new("refpt"), i));
}
reference_locator(ui, refi);
ui.add_space(4.0);
egui::Grid::new("xfp-ab-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
widgets::dim_label(ui, "X:");
+2 -7
View File
@@ -376,8 +376,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
}
/// The active tool's options in the Control bar: Mirror & Cut's axis and the side it keeps, Puppet
/// Warp's mesh and pins, the Artboard tool's Move and Scale Artwork with Artboard (set through
/// `tool.setOption`).
/// Warp's mesh and pins (set through `tool.setOption`). The Artboard tool's options are drawn by
/// [`crate::panels::artboards::control_bar`].
pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) {
if app.session.tool_id() == "dimension" {
return crate::dialogs::dimension::controls(app, ui);
@@ -394,11 +394,6 @@ pub fn control_bar_options(app: &mut VectorcraftApp, ui: &mut Ui) {
if app.session.tool_id() == vectorcraft_tools::cropimage::ID {
return crop_options(app, ui);
}
if app.session.tool_id() == "artboard" {
crate::panels::artboards::art_options(app, ui);
ui.separator();
return;
}
if app.session.tool_id() != "mirrorCut" {
return;
}
+1
View File
@@ -405,6 +405,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`.
- **Control bar with the Artboard tool:** while the Artboard tool is in use, the Control bar shows the active artboard's controls in place of the object controls: Preset, Portrait and Landscape, New Artboard, Delete Artboard, the Name field, Artboard Options, Rearrange All Artboards, the Transform link, Move and Scale Artwork with Artboard and, while the bar has room, the reference point with X and Y, and W and H with the Constrain Width and Height Proportions link. Each runs the command its Properties or Transform panel counterpart runs (`artboard.setProps`, `artboard.new`, `artboard.delete`, `tool.options {tool: "artboard"}`), and the reference point is the one the Transform panel uses. In an 800-point window the controls up to the Transform link fit, Move and Scale Artwork with Artboard show as their boxes alone with their names in tooltips, and the link opens the Transform panel with the artboard's position and size. Pointer and key tests in `panels/artboards.rs` cover the controls, W about the reference point, the orientation and the name. Public reference: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (archived copy).
- **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.