mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 21:23:02 +00:00
fix(ui): use vector path icon for shape layers in Properties (#10787)
* fix(ui): use vector path icon for shape layers in Properties * test(ui): verify rendered shape Properties icon size * test(ui): verify rendered shape icon identity * test(ui): verify painted icons for all layer kinds * test(ui): pin expected Properties icons for all layer kinds * fix(ui): align shape icon size and document attribution * ci: rerun checks * Enhance tests for shape icon transparency Added assertion to check for nontransparent pixels in shape icons. * Enhance tests for shape icon rasterization Add tests for shape icon rasterization and anchor visibility. * Add tests for shape properties and header icons Added tests for shape properties and header icons. * Fix attribution for Lucide icons in ATTRIBUTION.md
This commit is contained in:
1 parent
d0299c4179
commit
0143b14844
5 files changed
+103
-2
No files matched your search
+2
-1
@@ -15,8 +15,9 @@ next to them.
|
||||
| `epaint_default_fonts::EGUI_ICONS` (build input; deflated into `OUT_DIR/egui_fonts/egui-icons.ttf.deflate`, registered by `crates/ui-egui/src/text/egui_fonts.rs`) | egui-icons, five-glyph subset of emoji-icon-font | John Slegers | [egui fork at 6d518fc](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [original](https://github.com/jslegers/emoji-icon-font); egui subset, unmodified here | MIT; Copyright (c) 2014 John Slegers; full upstream notice in [NOTICE](NOTICE) |
|
||||
| `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`, `slice-select.svg`, `font-similar.svg`, `font-variable.svg`, `sampler.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `ellipse-dashed.svg`, `gradient.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `text-warp.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `history-brush.svg`, `quick-selection.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `convert-point.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg`, `path-op-*.svg` and `marquee-{row,column}.svg`, `align-edge-*.svg` and `distribute-*.svg`, `crop-overlay-*.svg`, `vertical-align-*.svg`, `spirit-level.svg` (116 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 `vector-shape.svg`, `slice-knife.svg`, `slice-select.svg`, `font-similar.svg`, `font-variable.svg`, `sampler.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `ellipse-dashed.svg`, `gradient.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `text-warp.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `history-brush.svg`, `quick-selection.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `convert-point.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg`, `path-op-*.svg` and `marquee-{row,column}.svg`, `align-edge-*.svg` and `distribute-*.svg`, `crop-overlay-*.svg`, `vertical-align-*.svg`, `spirit-level.svg` (116 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/vector-shape.svg` | Vector path shape-layer glyph with four anchor points | @superplay2021 | Original work for PhotoCraft | MIT OR Apache-2.0 |
|
||||
| `assets/icons/slice-knife.svg` | Slice tool glyph | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
| `assets/icons/slice-select.svg` | Slice Select tool glyph | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
| `assets/icons/crop-overlay-*.svg` | Crop overlay glyphs: Rule of Thirds, Grid, Diagonal, Triangle, Golden Ratio and Golden Spiral inside the crop-mark frame | PhotoCraft contributors | Original work | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M8 6H16 M18 8V16 M16 18H8 M6 16V8"/>
|
||||
<rect x="4" y="4" width="4" height="4"/>
|
||||
<rect x="16" y="4" width="4" height="4"/>
|
||||
<rect x="4" y="16" width="4" height="4"/>
|
||||
<rect x="16" y="16" width="4" height="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -184,6 +184,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
||||
("type-mask-vertical", include_bytes!("../../../assets/icons/type-mask-vertical.svg")),
|
||||
("undo-2", include_bytes!("../../../assets/icons/undo-2.svg")),
|
||||
("unlink", include_bytes!("../../../assets/icons/unlink.svg")),
|
||||
("vector-shape", include_bytes!("../../../assets/icons/vector-shape.svg")),
|
||||
("vertical-align-bottom", include_bytes!("../../../assets/icons/vertical-align-bottom.svg")),
|
||||
("vertical-align-middle", include_bytes!("../../../assets/icons/vertical-align-middle.svg")),
|
||||
("vertical-align-top", include_bytes!("../../../assets/icons/vertical-align-top.svg")),
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn kind_icon(content: &LayerContent) -> &'static str {
|
||||
LayerContent::Group(_) => "folder",
|
||||
LayerContent::Fill(_) => "paint-bucket",
|
||||
LayerContent::Text(_) => "type",
|
||||
LayerContent::Shape(_) => "pentagon",
|
||||
LayerContent::Shape(_) => "vector-shape",
|
||||
LayerContent::Smart(_) => "app-window",
|
||||
LayerContent::Raster(_) => "image",
|
||||
}
|
||||
|
||||
@@ -363,3 +363,95 @@ fn background_in_a_multiple_selection_keeps_align_and_distribute() {
|
||||
assert_eq!((b.x0 + b.x1) / 2, 150, "centred on the Background, which spans the canvas: {b:?}");
|
||||
assert_eq!(b.y0, 30, "only horizontally");
|
||||
}
|
||||
|
||||
/// Shape layers use the registered vector icon; raster and text mappings stay unchanged.
|
||||
#[test]
|
||||
fn shape_properties_uses_vector_shape_icon() {
|
||||
let (session, layers) = kinds();
|
||||
let app = PhotocraftApp::new(session, crate::Services::default());
|
||||
|
||||
assert!(crate::icons::exists("vector-shape"));
|
||||
|
||||
for (kind, id) in layers {
|
||||
let layer = layer_of(&app, id);
|
||||
let icon = kind_icon(&layer.content);
|
||||
|
||||
let expected_icon = match kind {
|
||||
"pixel" => "image",
|
||||
"shape" => "vector-shape",
|
||||
"type" => "type",
|
||||
_ => panic!("Unexpected layer kind: {kind}"),
|
||||
};
|
||||
|
||||
assert_eq!(icon, expected_icon, "{kind}: Properties icon");
|
||||
}
|
||||
}
|
||||
|
||||
/// Headers paint the expected 15 px icons, and the shape glyph has four hollow anchors.
|
||||
#[test]
|
||||
fn shape_header_paints_correct_icon() {
|
||||
let (session, layers) = kinds();
|
||||
let app = PhotocraftApp::new(session, crate::Services::default());
|
||||
|
||||
for (kind, id) in layers {
|
||||
let layer = layer_of(&app, id);
|
||||
let icon = kind_icon(&layer.content);
|
||||
let mut ready = false;
|
||||
let mut h = Harness::new_ui(move |ui| {
|
||||
if !ready {
|
||||
crate::theme::install_fonts(ui.ctx());
|
||||
egui_extras::install_image_loaders(ui.ctx());
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
header(ui, &layer);
|
||||
});
|
||||
h.run_steps(4);
|
||||
|
||||
let icon_sizes: Vec<_> = h
|
||||
.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|shape| match &shape.shape {
|
||||
egui::Shape::Rect(rect) if rect.brush.is_some() => Some(rect.rect.size()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let expected = 15.0;
|
||||
|
||||
assert_eq!(icon_sizes, vec![vec2(expected, expected)], "{kind}: Properties header icon size");
|
||||
|
||||
let expected_texture = h
|
||||
.ctx
|
||||
.try_load_texture(
|
||||
&format!("bytes://icons/{icon}.svg"),
|
||||
Default::default(),
|
||||
egui::load::SizeHint::Size { width: expected as u32, height: expected as u32, maintain_aspect_ratio: false },
|
||||
)
|
||||
.unwrap()
|
||||
.texture_id()
|
||||
.expect("icon texture loaded");
|
||||
|
||||
let painted_texture = h.output().shapes.iter().find_map(|shape| match &shape.shape {
|
||||
egui::Shape::Rect(rect) => rect.brush.as_ref().map(|brush| brush.fill_texture_id),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(painted_texture, Some(expected_texture), "{kind}: Properties header must paint {icon}");
|
||||
|
||||
if kind == "shape" {
|
||||
let image = crate::icons::rasterize_svg(icon, 15, 15).expect("shape icon rasterized");
|
||||
assert!(image.pixels.iter().any(|pixel| pixel.a() > 0), "shape icon must have nontransparent pixels");
|
||||
|
||||
// Native SVG dimensions keep anchor centres clear of antialiased edges.
|
||||
let anchors = crate::icons::rasterize_svg(icon, 24, 24).expect("shape anchors rasterized");
|
||||
for (x, y) in [(6, 6), (18, 6), (6, 18), (18, 18)] {
|
||||
assert_eq!(anchors[(x, y)].a(), 0, "anchor at ({x}, {y}) must be hollow");
|
||||
for (edge_x, edge_y) in [(x - 2, y), (x + 2, y), (x, y - 2), (x, y + 2)] {
|
||||
assert!(anchors[(edge_x, edge_y)].a() > 0, "anchor at ({x}, {y}) must have four visible sides");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user