diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 8c4a8e969b..d994b18af6 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -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) | | 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 | | 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 | (`icons/.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 | (`icons/.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 | 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 | diff --git a/assets/icons/vector-shape.svg b/assets/icons/vector-shape.svg new file mode 100644 index 0000000000..e05c3b8c1d --- /dev/null +++ b/assets/icons/vector-shape.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs index c29e2192fa..3dd68269fb 100644 --- a/crates/ui-egui/src/icon_data.rs +++ b/crates/ui-egui/src/icon_data.rs @@ -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")), diff --git a/crates/ui-egui/src/panels/props_layout.rs b/crates/ui-egui/src/panels/props_layout.rs index 8d604995bc..e74faec053 100644 --- a/crates/ui-egui/src/panels/props_layout.rs +++ b/crates/ui-egui/src/panels/props_layout.rs @@ -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", } diff --git a/crates/ui-egui/src/panels/props_layout/tests.rs b/crates/ui-egui/src/panels/props_layout/tests.rs index 50e9006875..cc2cd61cee 100644 --- a/crates/ui-egui/src/panels/props_layout/tests.rs +++ b/crates/ui-egui/src/panels/props_layout/tests.rs @@ -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"); + } + } + } + } +}