From a672bd70dd2da3aaa72139558a731b8387189a69 Mon Sep 17 00:00:00 2001 From: Murat Vural Date: Sun, 11 Oct 2026 02:14:14 +0300 Subject: [PATCH] feat(ui): add horizontal and vertical center align button # Conflicts: # crates/engine/src/tests.rs --- ASSETS.md | 1 + assets/icons/dc-al-center.svg | 1 + crates/engine/src/tests.rs | 13 +++++++++++++ crates/ui-egui/src/icon_data.rs | 1 + crates/ui-egui/src/menus.rs | 8 +++++--- crates/ui-egui/src/panels/align.rs | 22 ++++++++++++++++++++-- 6 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 assets/icons/dc-al-center.svg diff --git a/ASSETS.md b/ASSETS.md index bf15723a..1003a0e0 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -83,6 +83,7 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/icons/copy.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/dc-actions.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-al-bottom.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-center.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-al-hcenter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-al-left.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-al-right.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | diff --git a/assets/icons/dc-al-center.svg b/assets/icons/dc-al-center.svg new file mode 100644 index 00000000..2e8e9e97 --- /dev/null +++ b/assets/icons/dc-al-center.svg @@ -0,0 +1 @@ + diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8071a7ed..6a66ad1a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -506,6 +506,19 @@ fn align_to_artboard_uses_the_artboard_named() { assert_eq!(x0(&s, c), boards[0].x0, "off every artboard: the first"); } +#[test] +fn align_both_centers() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0); + let b = rect(&mut s, 20.0, 40.0, 20.0, 20.0); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + s.execute("object.align", &json!({"horizontal": "center", "vertical": "center"})).unwrap(); + let gb_a = s.doc().unwrap().doc.node(a).unwrap().geometric_bounds().unwrap(); + let gb_b = s.doc().unwrap().doc.node(b).unwrap().geometric_bounds().unwrap(); + assert!((gb_a.center().x - gb_b.center().x).abs() < 1e-6); + assert!((gb_a.center().y - gb_b.center().y).abs() < 1e-6); +} + /// #541: with the Selection tool, a click on one object of the selection makes it the key object: /// aligning to the key moves the others to it, Distribute Spacing spaces them from it, and a click /// on the key again lets it go. diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs index f2a97a7c..016b4722 100644 --- a/crates/ui-egui/src/icon_data.rs +++ b/crates/ui-egui/src/icon_data.rs @@ -31,6 +31,7 @@ pub static ICONS: &[(&str, &[u8])] = &[ ("copy", include_bytes!("../../../assets/icons/copy.svg")), ("dc-actions", include_bytes!("../../../assets/icons/dc-actions.svg")), ("dc-al-bottom", include_bytes!("../../../assets/icons/dc-al-bottom.svg")), + ("dc-al-center", include_bytes!("../../../assets/icons/dc-al-center.svg")), ("dc-al-hcenter", include_bytes!("../../../assets/icons/dc-al-hcenter.svg")), ("dc-al-left", include_bytes!("../../../assets/icons/dc-al-left.svg")), ("dc-al-right", include_bytes!("../../../assets/icons/dc-al-right.svg")), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 6bf171ae..6254bd3c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -308,13 +308,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "window.panel.float", "Float Panel", "", - "{panel: id or \"tools\", x?, y?, onto?: id, below?: id, above?: id, group?: bool, collapsed?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it as a tab with the floating group holding that panel instead; `below` / `above` puts its group in a set with that group, right below or above it, as dropping it on that group's lower or upper half does (the set's groups stack top to bottom and move together); `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved (out of its set, given x, y); `collapsed` shows its group's tabs only, as a double-click on a tab does; a docked group floats its dock column. \"tools\" floats the Tools panel. Returns {panel, floating, group, set, pos, collapsed, column}", + "{panel: id or \"tools\", x?, y?, onto?: id, group?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it with the floating group holding that panel instead; `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved. \"tools\" floats the Tools panel. Returns {panel, floating, group, pos}", ), ( "window.panel.dock", "Dock Panel", "", - "{panel: id or \"tools\", group?: bool, column?: true or n} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge. Returns {panel, floating, column}", + "{panel: id or \"tools\", group?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; \"tools\" docks the Tools panel at the window's left edge", ), ( "window.collapseDock", @@ -1181,7 +1181,6 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option Vec<(&'static str, Vec)> { cp("Horizontal Align Left", "object.align", json!({"horizontal": "left"})), cp("Horizontal Align Center", "object.align", json!({"horizontal": "center"})), cp("Horizontal Align Right", "object.align", json!({"horizontal": "right"})), + Sep, + cp("Horizontal & Vertical Align Center", "object.align", json!({"horizontal": "center", "vertical": "center"})), + Sep, cp("Vertical Align Top", "object.align", json!({"vertical": "top"})), cp("Vertical Align Center", "object.align", json!({"vertical": "center"})), cp("Vertical Align Bottom", "object.align", json!({"vertical": "bottom"})), diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index 40b9d23a..a24f191f 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -58,14 +58,20 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [ pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) { let n = selection_len(app); let to = align_to(app, ui.ctx()); - for (icon, tip, axis, v) in [ + for (i, (icon, tip, axis, v)) in [ ("align-start-vertical", "Horizontal Align Left", "horizontal", "left"), ("align-center-vertical", "Horizontal Align Center", "horizontal", "center"), ("align-end-vertical", "Horizontal Align Right", "horizontal", "right"), ("align-start-horizontal", "Vertical Align Top", "vertical", "top"), ("align-center-horizontal", "Vertical Align Center", "vertical", "center"), ("align-end-horizontal", "Vertical Align Bottom", "vertical", "bottom"), - ] { + ] + .into_iter() + .enumerate() + { + if i == 3 && widgets::icon_button(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, size).clicked() { + app.run("object.align", align_params(json!({"horizontal": "center", "vertical": "center"}), to, n)).ok(); + } if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() { app.run("object.align", align_params(json!({axis: v}), to, n)).ok(); } @@ -98,6 +104,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { for (i, (icon, tip, axis, v)) in ALIGN.iter().enumerate() { if i == 3 { ui.add_space(6.0); + if widgets::icon_button_enabled(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, n >= 1, 32.0).clicked() { + app.run("object.align", align_params(json!({"horizontal": "center", "vertical": "center"}), to, n)).ok(); + } + ui.add_space(6.0); } if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 1, 32.0).clicked() { app.run("object.align", align_params(json!({*axis: v}), to, n)).ok(); @@ -247,6 +257,14 @@ mod tests { assert_eq!(bounds(&app).x0, 0.0, "the artboard's left edge"); } + #[test] + fn align_both_centers_params() { + let p = align_params(json!({"horizontal": "center", "vertical": "center"}), AlignTo::Selection, 2); + assert_eq!(p["horizontal"], "center"); + assert_eq!(p["vertical"], "center"); + assert_eq!(p["to"], "selection"); + } + /// #541: while the selection has a key object Align aligns to it, whatever Align To says; /// choosing Align to Selection lets the key go. #[test]