Move tool: show all six Align icons and an icon menu for Distribute (#3490)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-11 16:44:26 +02:00
co-authored by Claude Opus 5.5
parent 16bdcbd54e
commit 0a75c37c7a
19 changed files with 342 additions and 29 deletions
+2 -1
View File
@@ -20,7 +20,7 @@ next to them.
| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) |
| `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | <https://github.com/rsms/inter> | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) |
| `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono 2.305 (numeric font) | The JetBrains Mono Project Authors | <https://github.com/JetBrains/JetBrainsMono> | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) |
| `assets/icons/*.svg` except `slice-knife.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg` and `marquee-{row,column}.svg` (113 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
| `assets/icons/*.svg` except `slice-knife.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg` and `marquee-{row,column}.svg`, `align-edge-*.svg` and `distribute-*.svg` (113 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | <https://github.com/lucide-icons/lucide> (`icons/<name>.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
| `assets/icons/{check,chevron-down,chevron-left,chevron-right,chevron-up,chevrons-left,chevrons-right,circle,clock,compass,info,link,link-2,lock,minus,monitor,moon,move,navigation,plus,search,square,trash,triangle,type,x,zoom-in}.svg` | Lucide icons derived from Feather (subset of the row above) | Cole Bemis (Feather), Lucide Contributors | <https://github.com/feathericons/feather> via Lucide | MIT (Feather) and ISC (Lucide), [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
| `assets/icons/slice-knife.svg` | Slice tool glyph | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/artboard.svg` | Artboard tool glyph: a board with a tick at each end of its edges | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
@@ -29,6 +29,7 @@ next to them.
| `assets/icons/marquee-row.svg`, `assets/icons/marquee-column.svg` | Single Row and Single Column Marquee tool glyphs: a dashed row (column) between two edge bars, drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/clip-below.svg`, `assets/icons/clip-release.svg` | Clipping-mask cursors (⌥ over the line between two layers): a hooked arrow onto the layer below; the same with a small "no" badge to release | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/layer-mask.svg`, `assets/icons/adjustment-layer.svg` | Layers panel footer glyphs: Add a mask (a frame around a solid disc) and New Fill or Adjustment Layer (a circle, one half solid), drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/align-edge-*.svg`, `assets/icons/distribute-*.svg` | Move tool options-bar glyphs: Align (six edges/centres) and Distribute (edges, centres, spacing), a guide line or edge lines with bars, drawn on Lucide's 24 px grid and stroke | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/frame-placeholder.svg` | Frame tool glyph: an empty placeholder frame, crossed corner to corner | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
| `assets/icons/eraser-background.svg`, `assets/icons/eraser-magic.svg` | Background Eraser and Magic Eraser tool glyphs: Lucide `eraser` plus added marks | Lucide Icons and Contributors; marks by PhotoCraft contributors | Derived from <https://github.com/lucide-icons/lucide> `icons/eraser.svg` | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
| `assets/icons/brush-replace.svg` | Color Replacement tool glyph: Lucide `brush` plus two swap arrows | Lucide Icons and Contributors; arrows by PhotoCraft contributors | Derived from <https://github.com/lucide-icons/lucide> `icons/brush.svg` | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) |
+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M3 20h18" />
<rect x="5" y="4" width="5" height="12" rx="1" />
<rect x="14" y="9" width="5" height="7" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M12 3v18" />
<rect x="5" y="5" width="14" height="5" rx="1" />
<rect x="8" y="14" width="8" height="5" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M4 3v18" />
<rect x="8" y="5" width="12" height="5" rx="1" />
<rect x="8" y="14" width="7" height="5" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M20 3v18" />
<rect x="4" y="5" width="12" height="5" rx="1" />
<rect x="9" y="14" width="7" height="5" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M3 4h18" />
<rect x="5" y="8" width="5" height="12" rx="1" />
<rect x="14" y="8" width="5" height="7" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M3 12h18" />
<rect x="5" y="5" width="5" height="14" rx="1" />
<rect x="14" y="8" width="5" height="8" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

+19
View File
@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M4 7h16" />
<rect x="7" y="2" width="10" height="3" rx="1" />
<path d="M4 14h16" />
<rect x="7" y="9" width="10" height="3" rx="1" />
<path d="M4 21h16" />
<rect x="7" y="16" width="10" height="3" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 516 B

+22
View File
@@ -0,0 +1,22 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<rect x="2" y="6" width="4" height="12" rx="1" />
<path d="M4 2v2" />
<path d="M4 20v2" />
<rect x="10" y="6" width="4" height="12" rx="1" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<rect x="18" y="6" width="4" height="12" rx="1" />
<path d="M20 2v2" />
<path d="M20 20v2" />
</svg>

After

Width:  |  Height:  |  Size: 585 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M3 4v16" />
<path d="M21 4v16" />
<rect x="9" y="7" width="6" height="10" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 387 B

+19
View File
@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M3 4v16" />
<rect x="5" y="7" width="3" height="10" rx="1" />
<path d="M10 4v16" />
<rect x="12" y="7" width="3" height="10" rx="1" />
<path d="M17 4v16" />
<rect x="19" y="7" width="3" height="10" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 517 B

+19
View File
@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M7 4v16" />
<rect x="2" y="7" width="3" height="10" rx="1" />
<path d="M14 4v16" />
<rect x="9" y="7" width="3" height="10" rx="1" />
<path d="M21 4v16" />
<rect x="16" y="7" width="3" height="10" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 516 B

+19
View File
@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M4 3h16" />
<rect x="7" y="5" width="10" height="3" rx="1" />
<path d="M4 10h16" />
<rect x="7" y="12" width="10" height="3" rx="1" />
<path d="M4 17h16" />
<rect x="7" y="19" width="10" height="3" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 517 B

+22
View File
@@ -0,0 +1,22 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<rect x="6" y="2" width="12" height="4" rx="1" />
<path d="M2 4h2" />
<path d="M20 4h2" />
<rect x="6" y="10" width="12" height="4" rx="1" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<rect x="6" y="18" width="12" height="4" rx="1" />
<path d="M2 20h2" />
<path d="M20 20h2" />
</svg>

After

Width:  |  Height:  |  Size: 585 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- PhotoCraft's own Move tool options-bar glyph (not from an icon set). -->
<path d="M4 3h16" />
<path d="M4 21h16" />
<rect x="7" y="9" width="10" height="6" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 387 B

+76
View File
@@ -0,0 +1,76 @@
//! The Move tool's Align and Distribute controls (Photoshop shows them whenever the Move tool is
//! active; they enable once enough layers are selected). Every button dispatches an existing
//! `layer.align.*` / `layer.distribute.*` command.
use serde_json::json;
use crate::theme::Tokens;
use crate::{PhotocraftApp, icons};
/// (command id, icon, label). The labels are the Layer menu's, already translated.
const ALIGN: [(&str, &str, &str); 6] = [
("layer.align.leftEdges", "align-edge-left", "Left Edges"),
("layer.align.horizontalCenters", "align-edge-hcenter", "Horizontal Centers"),
("layer.align.rightEdges", "align-edge-right", "Right Edges"),
("layer.align.topEdges", "align-edge-top", "Top Edges"),
("layer.align.verticalCenters", "align-edge-vcenter", "Vertical Centers"),
("layer.align.bottomEdges", "align-edge-bottom", "Bottom Edges"),
];
const DISTRIBUTE: [(&str, &str, &str); 8] = [
("layer.distribute.topEdges", "distribute-top", "Top Edges"),
("layer.distribute.verticalCenters", "distribute-vcenter", "Vertical Centers"),
("layer.distribute.bottomEdges", "distribute-bottom", "Bottom Edges"),
("layer.distribute.leftEdges", "distribute-left", "Left Edges"),
("layer.distribute.horizontalCenters", "distribute-hcenter", "Horizontal Centers"),
("layer.distribute.rightEdges", "distribute-right", "Right Edges"),
("layer.distribute.horizontally", "distribute-horizontal", "Horizontally"),
("layer.distribute.vertically", "distribute-vertical", "Vertically"),
];
/// The six align icons followed by a menu holding both groups with icons and names.
pub fn options(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
ui.spacing_mut().item_spacing.x = 0.0;
for (cmd, icon, label) in ALIGN {
let tip = format!("{}: {}", tl!("Align"), tl!(label));
let on = app.session.is_enabled(cmd);
if ui.add_enabled_ui(on, |ui| icons::button(ui, icon, 24.0, false, &tip)).inner.clicked() {
let _ = app.run(cmd, json!({}));
}
}
ui.add_space(6.0);
let more = icons::button(ui, "ellipsis", 24.0, false, tl!("Align and distribute"));
egui::Popup::menu(&more).show(|ui| {
ui.set_min_width(200.0);
let mut section = |ui: &mut egui::Ui, title: &str, items: &[(&str, &str, &str)]| {
ui.label(egui::RichText::new(title).small().color(t.text_dim));
for (cmd, icon, label) in items {
let on = app.session.is_enabled(cmd);
let tint = if on { t.icon } else { t.text_dim };
let button = egui::Button::image_and_text(icons::image(icon, 16.0, tint), tl!(label));
if ui.add_enabled(on, button).clicked() {
let _ = app.run(cmd, json!({}));
ui.close();
}
}
};
section(ui, tl!("Align"), &ALIGN);
ui.separator();
section(ui, tl!("Distribute"), &DISTRIBUTE);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_button_is_a_live_command_with_an_icon() {
let specs = photocraft_engine::command_specs();
for (cmd, icon, _) in ALIGN.iter().chain(DISTRIBUTE.iter()) {
assert!(specs.iter().any(|c| c.id == *cmd), "{cmd} is not a command");
assert!(icons::exists(icon), "{icon} is not embedded");
}
}
}
+14
View File
@@ -6,6 +6,20 @@ pub static ICONS: &[(&str, &[u8])] = &[
("align-justify", include_bytes!("../../../assets/icons/align-justify.svg")),
("align-left", include_bytes!("../../../assets/icons/align-left.svg")),
("align-right", include_bytes!("../../../assets/icons/align-right.svg")),
("align-edge-bottom", include_bytes!("../../../assets/icons/align-edge-bottom.svg")),
("align-edge-hcenter", include_bytes!("../../../assets/icons/align-edge-hcenter.svg")),
("align-edge-left", include_bytes!("../../../assets/icons/align-edge-left.svg")),
("align-edge-right", include_bytes!("../../../assets/icons/align-edge-right.svg")),
("align-edge-top", include_bytes!("../../../assets/icons/align-edge-top.svg")),
("align-edge-vcenter", include_bytes!("../../../assets/icons/align-edge-vcenter.svg")),
("distribute-bottom", include_bytes!("../../../assets/icons/distribute-bottom.svg")),
("distribute-hcenter", include_bytes!("../../../assets/icons/distribute-hcenter.svg")),
("distribute-horizontal", include_bytes!("../../../assets/icons/distribute-horizontal.svg")),
("distribute-left", include_bytes!("../../../assets/icons/distribute-left.svg")),
("distribute-right", include_bytes!("../../../assets/icons/distribute-right.svg")),
("distribute-top", include_bytes!("../../../assets/icons/distribute-top.svg")),
("distribute-vcenter", include_bytes!("../../../assets/icons/distribute-vcenter.svg")),
("distribute-vertical", include_bytes!("../../../assets/icons/distribute-vertical.svg")),
("app-window", include_bytes!("../../../assets/icons/app-window.svg")),
("artboard", include_bytes!("../../../assets/icons/artboard.svg")),
("arrow-left-right", include_bytes!("../../../assets/icons/arrow-left-right.svg")),
+1
View File
@@ -19,6 +19,7 @@ pub mod adjust_dialog;
pub mod adjust_editors;
pub mod adjust_preview;
pub mod adjust_ui;
mod align_bar;
mod alt_grab;
pub mod analysis_ui;
pub mod anchor_tools;
+1 -28
View File
@@ -1282,34 +1282,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
widgets::vline(ui, 22.0);
widgets::checkbox(ui, &mut o.move_show_transform, tl!("Show Transform Controls"));
widgets::vline(ui, 22.0);
ui.spacing_mut().item_spacing.x = 0.0;
for (icon, tip, cmd) in [
("panels-top-left", tl!("Align left edges"), "layer.align.leftEdges"),
("app-window", tl!("Align horizontal centers"), "layer.align.horizontalCenters"),
("panel-right", tl!("Align right edges"), "layer.align.rightEdges"),
] {
let on = app.session.is_enabled(cmd);
if ui.add_enabled_ui(on, |ui| icons::button(ui, icon, 24.0, false, tip)).inner.clicked() {
let _ = app.run(cmd, json!({}));
}
}
ui.add_space(6.0);
let more = icons::button(ui, "ellipsis", 24.0, false, tl!("Align and distribute"));
egui::Popup::menu(&more).show(|ui| {
ui.set_min_width(200.0);
let mut section = |ui: &mut egui::Ui, title: &str, prefix: &str| {
ui.label(egui::RichText::new(tl!(&title)).small().color(t.text_dim));
for c in photocraft_engine::command_specs().iter().filter(|c| c.id.starts_with(prefix)) {
if ui.add_enabled(app.session.is_enabled(c.id), egui::Button::new(tl!(c.label))).clicked() {
let _ = app.run(c.id, json!({}));
ui.close();
}
}
};
section(ui, tl!("Align"), "layer.align.");
ui.separator();
section(ui, tl!("Distribute"), "layer.distribute.");
});
crate::align_bar::options(app, ui);
}
Tool::RectMarquee | Tool::EllipseMarquee => hint(
ui,