From 9c2002777865e9dae236c085a235152dd087cd97 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 21 Oct 2026 23:32:48 +0700 Subject: [PATCH] Keyboard Shortcuts: each Align and Distribute button of the Align panel can be given a key Squashed from #3317 onto main. --- crates/ui-egui/src/menus.rs | 92 ++++++++++ crates/ui-egui/src/panels/align.rs | 259 ++++++++++++++++++++++++++--- crates/ui-egui/src/state.rs | 6 + docs/mcp.md | 12 ++ docs/target-app-parity.md | 1 + docs/ui-parity.md | 4 +- 6 files changed, 350 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index b0f8042d3..0c59b761f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -224,6 +224,96 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("type.alignRight", "Align right", "Cmd+Shift+R", "{} align the selected type's paragraphs (or those the Type tool's selection touches) right"), ("type.justifyLeft", "Justify with last line aligned left", "Cmd+Shift+J", "{} justify the selected type's paragraphs, last line aligned left"), ("type.justifyAll", "Justify all lines", "Cmd+Shift+F", "{} justify every line of the selected type's paragraphs"), + ( + "align.horizontalLeft", + "Horizontal Align Left", + "", + "{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.horizontalCenter", + "Horizontal Align Center", + "", + "{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.horizontalRight", + "Horizontal Align Right", + "", + "{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.horizontalVerticalCenter", + "Horizontal & Vertical Align Center", + "", + "{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.verticalTop", + "Vertical Align Top", + "", + "{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.verticalCenter", + "Vertical Align Center", + "", + "{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.verticalBottom", + "Vertical Align Bottom", + "", + "{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice", + ), + ( + "align.distributeVerticalTop", + "Vertical Distribute Top", + "", + "{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeVerticalCenter", + "Vertical Distribute Center", + "", + "{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeVerticalBottom", + "Vertical Distribute Bottom", + "", + "{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeHorizontalLeft", + "Horizontal Distribute Left", + "", + "{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeHorizontalCenter", + "Horizontal Distribute Center", + "", + "{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeHorizontalRight", + "Horizontal Distribute Right", + "", + "{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeVerticalSpace", + "Vertical Distribute Space", + "", + "{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), + ( + "align.distributeHorizontalSpace", + "Horizontal Distribute Space", + "", + "{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key", + ), ( "type.bold", "Bold", @@ -1248,6 +1338,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option { crate::panels::paragraph::align(app, justify) } + id if let Some(b) = crate::panels::align::button(id) => crate::panels::align::run_button(app, b), "view.gradientAnnotator" => flag(&mut app.ui.view.gradient_annotator), "view.livePaintGaps" => flag(&mut app.ui.view.live_paint_gaps), "effect.last" => match app.last_effect.clone() { @@ -2304,6 +2395,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { "file.save" => app.session.active().is_some_and(|d| d.path.is_none() || d.converted || d.is_dirty()), "type.bold" | "type.italic" => crate::panels::character::text_style(app).is_some(), id if crate::panels::paragraph::ALIGN_COMMANDS.iter().any(|c| c.0 == id) => crate::panels::character::text_style(app).is_some(), + id if let Some(b) = crate::panels::align::button(id) => enabled(app, b.command()), "file.reveal" => app.services.reveal.is_some() && app.session.active().is_some_and(|d| d.path.is_some()), "file.cancelExport" | "file.exportInBackground" => app.background.jobs.iter().any(|j| j.progress.is_some() && !j.cancelled()), "file.place" diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index aa98afbd0..36cafab8f 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -1,5 +1,6 @@ //! Align panel: Align Objects, Distribute Objects and Distribute Spacing (both with the spacing -//! value when aligning to a key object) and Align To (selection / key object / artboard). +//! value when aligning to a key object or a key guide) and Align To (selection / key object / +//! artboard). use egui::Ui; use serde::Serialize; @@ -51,9 +52,9 @@ pub fn align_params(base: Value, to: AlignTo, n_selected: usize) -> Value { } /// `object.align` params `base` with the `to` that the align buttons of the Align panel, the Control -/// bar and the Properties panel send: the artboard for a single selected object, else the key object -/// while there is one, else the Align To choice. Object → Align sends it too while every selected -/// object is selected whole ([`crate::menus::invoke`]). +/// bar and the Properties panel send: the key while there is one (a key object, or a key guide: +/// #2143), else the artboard for a single selected object, else the Align To choice. Object → Align +/// sends it too while every selected object is selected whole ([`crate::menus::invoke`]). pub fn with_align_to(app: &VectorcraftApp, base: Value) -> Value { align_params(base, align_to(app), objects_aligned(app)) } @@ -88,6 +89,85 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [ ("dc-dist-right", "Horizontal Distribute Right", "horizontal", "right"), ]; +/// What a button of the Align panel's Align Objects, Distribute Objects and Distribute Spacing +/// rows runs ([`run_button`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Button { + /// An Align Objects button: `object.align {axis: value}`. + Align(&'static str, &'static str), + /// Horizontal & Vertical Align Center: `object.align {horizontal: "center", vertical: "center"}`. + AlignCenters, + /// A Distribute Objects button: `object.distribute {axis: value}`. + Distribute(&'static str, &'static str), + /// A Distribute Spacing button: `object.distributeSpacing {axis}`. + Spacing(&'static str), +} + +impl Button { + /// The engine command the button runs. + pub(crate) fn command(self) -> &'static str { + match self { + Button::Align(..) | Button::AlignCenters => "object.align", + Button::Distribute(..) => "object.distribute", + Button::Spacing(_) => "object.distributeSpacing", + } + } +} + +/// The Align panel's buttons as UI commands ([`crate::menus::UI_COMMANDS`]), in the panel's order: +/// (command id, button). Edit › Keyboard Shortcuts lists each under Align with its button's label, +/// with no default shortcut, and a shortcut runs what a click on the button runs. +pub const COMMANDS: [(&str, Button); 15] = [ + ("align.horizontalLeft", Button::Align("horizontal", "left")), + ("align.horizontalCenter", Button::Align("horizontal", "center")), + ("align.horizontalRight", Button::Align("horizontal", "right")), + ("align.horizontalVerticalCenter", Button::AlignCenters), + ("align.verticalTop", Button::Align("vertical", "top")), + ("align.verticalCenter", Button::Align("vertical", "center")), + ("align.verticalBottom", Button::Align("vertical", "bottom")), + ("align.distributeVerticalTop", Button::Distribute("vertical", "top")), + ("align.distributeVerticalCenter", Button::Distribute("vertical", "center")), + ("align.distributeVerticalBottom", Button::Distribute("vertical", "bottom")), + ("align.distributeHorizontalLeft", Button::Distribute("horizontal", "left")), + ("align.distributeHorizontalCenter", Button::Distribute("horizontal", "center")), + ("align.distributeHorizontalRight", Button::Distribute("horizontal", "right")), + ("align.distributeVerticalSpace", Button::Spacing("vertical")), + ("align.distributeHorizontalSpace", Button::Spacing("horizontal")), +]; + +/// The button of Align panel command `id` ([`COMMANDS`]), which [`run_button`] runs and which is +/// enabled while the engine command it runs is. `None`: `id` is not one of them. +pub(crate) fn button(id: &str) -> Option