From 3eb3a26789b910f6e611e51a2c8a9b318a665f11 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 8 May 2026 11:07:31 -0700 Subject: [PATCH] =?UTF-8?q?feat(app):=20wire=20desktop=20window=20icon=20?= =?UTF-8?q?=E2=80=94=20Terminal=20=E2=96=8CRS=20mark=20at=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Resume-prompt Option A (the post-v0.21.0 first option). Half-day desktop work, no cert dependency. Three deliverables: 1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`) — square Terminal mark: `#151515` background, brick-red `#a54242` 1 px border, brick-red ▌ cursor block centered, "RS" monogram in `#d0d0d0` foreground gray beneath. Same shape that already lives on the splash boot screen and card-back monogram, reused as the project's signature visual mark. Authored in a 64-unit logical box so it scales cleanly at every rasterisation target. 2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024 px) regenerated by `solitaire_engine/examples/icon_generator.rs` into `assets/icon/icon_.png`. Sizes cover Linux hicolor (16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16, 32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256, 512, 1024). The runtime path uses just the 256 px slot; the smaller sizes are pre-rendered for downstream packaging. 3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`). Bevy 0.18 has no `Window::icon` field — the icon is set through the underlying `winit::window::Window` via the `WinitWindows` resource. `set_window_icon` runs each Update tick, retries silently until `WinitWindows` is populated (typically frame 1 or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds a `winit::window::Icon`, and self-disables via `Local`. Same one-shot pattern as `apply_smart_default_window_size`. Desktop-only — Android draws its launcher icon from the APK manifest, so the system is target-gated to `cfg(not(target_os = "android"))`. Dep changes (CLAUDE.md §8 user-confirmed): - `winit = "0.30"` promoted from a transitive Bevy dep to a direct dep on `solitaire_app` so `winit::window::Icon` is in scope — bevy_winit 0.18 doesn't re-export it. Version pinned to whatever Bevy uses; if Bevy bumps winit, this line bumps in lockstep. - `tiny-skia` added as a direct dep on `solitaire_app` for PNG → RGBA decode. Already in workspace deps for `solitaire_engine`; no version drift risk. - Both new deps target-gated to non-Android only. Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs` hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED → panic with hashes formatted as Rust source → paste back in) handles future intentional builder edits cleanly. Workspace clippy + cargo test --workspace clean. 1185 passing (+1 from v0.21.0's 1184 baseline — the icon pin's `rasterised_icon_bytes_match_pinned_hashes`). Out of scope for this commit: `.icns` / `.ico` bundling for macOS / Windows app packaging. Both are packaging-time concerns (set via bundle manifests, not runtime calls) and would need new deps (`ico` and `icns` crates) — separate followup if/when the project ships as a packaged macOS / Windows app rather than just `cargo run`. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 2 + assets/icon/icon_1024.png | Bin 0 -> 17181 bytes assets/icon/icon_128.png | Bin 0 -> 1819 bytes assets/icon/icon_16.png | Bin 0 -> 263 bytes assets/icon/icon_24.png | Bin 0 -> 369 bytes assets/icon/icon_256.png | Bin 0 -> 3673 bytes assets/icon/icon_32.png | Bin 0 -> 489 bytes assets/icon/icon_48.png | Bin 0 -> 759 bytes assets/icon/icon_512.png | Bin 0 -> 7669 bytes assets/icon/icon_64.png | Bin 0 -> 927 bytes solitaire_app/Cargo.toml | 27 +++-- solitaire_app/src/lib.rs | 75 +++++++++++++ solitaire_engine/examples/icon_generator.rs | 67 ++++++++++++ solitaire_engine/src/assets/icon_svg.rs | 70 ++++++++++++ solitaire_engine/src/assets/mod.rs | 1 + solitaire_engine/tests/icon_svg_pin.rs | 114 ++++++++++++++++++++ 16 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 assets/icon/icon_1024.png create mode 100644 assets/icon/icon_128.png create mode 100644 assets/icon/icon_16.png create mode 100644 assets/icon/icon_24.png create mode 100644 assets/icon/icon_256.png create mode 100644 assets/icon/icon_32.png create mode 100644 assets/icon/icon_48.png create mode 100644 assets/icon/icon_512.png create mode 100644 assets/icon/icon_64.png create mode 100644 solitaire_engine/examples/icon_generator.rs create mode 100644 solitaire_engine/src/assets/icon_svg.rs create mode 100644 solitaire_engine/tests/icon_svg_pin.rs diff --git a/Cargo.lock b/Cargo.lock index 2af1fbe..589aca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6957,6 +6957,8 @@ dependencies = [ "keyring", "solitaire_data", "solitaire_engine", + "tiny-skia 0.12.0", + "winit", ] [[package]] diff --git a/assets/icon/icon_1024.png b/assets/icon/icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..592699f777784ce42a5f994e40eb193861b71f45 GIT binary patch literal 17181 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 Ir%8YQ5A={er~m)} literal 0 HcmV?d00001 diff --git a/assets/icon/icon_128.png b/assets/icon/icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ebb7168a8763d4cd05ce314e06d13d2e67959f36 GIT binary patch literal 1819 zcmciD`B&130tN6d;?j#yaZ62cUyciE<<^YkE^1<0J#{Q2Q`3Qf$`Hkca7umUmZo7M zQZA8O;;2!%pf*}gL+(~CX&Pq~m&9Bi2(S4E=C^t0eD1w}!p%8}_Ji)%-46f&)F0&? z`hBT?WuM~r?nPc01OUZ(e{YY|jFt^hq&d zB$D^Q>?ex)h@6?wOgS|GvO(0$Uzl_6`s7>yg_$kp%5+7-}1Xu z`vS6m@UW{xsV|O=m}QpTM`^m97k)y8P4JDcYKiG* z(NgkPPw7t)g_j631EO|B0#>B6kSd#URgWJ!2M?BO%q1kRpeNjAIC z@cg_?9e^7Aj2xCOa$WQOTM!5C({k{w7df0)2d0rxAtKj}UP$z9ZOTRbqF^_pY?Ob? zPLey9rPasRU3XA@+QWWA~H4y%CykWe;Rc z_8O}U@kC=|t@?vMPhK3~;`Y>Lee<2Y@&Ten%SKvKul_^U#emh013aU4xr;%ELG8ho zS#tjmes2l#3A=BZNjG)eT{ZsJ4H=@cy4pt>{$u#&)bd1AEQz$$f!to7>m42%noCc= zcCDpqBq1U^ykK`XnO;#*@>G=!LYKQ3)dn*W6ex`(`Vy49v(Booug7WcNTulsSgf_n zYMKhWF{)!J0G4>V7`QVZSc>3!)Ya8lACkYr4B)1#NYFi7Q~r^(Bo~C?NzR42O*>R!O0N0)hX_&b-2m<;0JaqrOTRz z!P>MYU*PyFd6Zc9SeNPrpPPX6x2DtE$QAkw>yJlQY|I1ipERPfWrrG|2cS@{02&W( zI``rWzN4c9zozQsIVS(m;fIvXc%BX5v8kx+fjBcO5C|5hxYt$(oSEPP1^dO`TfY$# z6OU?Ofo|PDm#)$zqeA+TOHOC^SvfXXXVH<-j!#ct%(;HEQWpG#^#-VRue87ZF<$c> z#G7|Z3Nprsw!jv_6^RG4B}eX5!k)l^ptFbRiOvMCtVAh-w@vHvDhSbiN?lliITa<7 zeqgC}58ks5+du5u)^HbH8;ikU_S`sL?`m(eGyw*(%n|HM;I4t30>jI?-a!ET)U)Oa z4ong{3(|HnK~0V;c-jI>&c`yqZ`|3%=fc7SPPcbBkEc>+Be&Z(Uh?opn=L@%M*P0F zFX0gY#7kO3B$FqhKRTgkmy~`l^#}T4sZDzE%Cqg(m?gZnv!G-e zY!)|NH6zxk-1|jG(~MetOU_bBPLj*mb95Z*Jjn6V`;YH*SfD8sRPkI1i%nXZwKx+e zo9S4sGucp+t6Kl^`Et001^brbhpnLTHfcXNf!0Q#c*rZmOo!hn?>R^&j15nrhgV-R%6O5vVmc4=st^ z3&nmMpJ-^XQ81E!1aJ1a1H<;|_1mubv_F3icQw9D$Nd{|JlU9knEqti{{2khn$F~@ zw4$P%hY*jUaGM)kBZQ=;b!@kuOMh?JZJ3vB6rDhmdDx79?x-;YYvc8gr%zugw`k+ZOXZwWJpoT8; zl*rkZdcl;AE5&IgfEQUXH%}jY(Ey36mZynKy1{Sj-x!hHl|U5lmv87e4!`<-dVs$V K+Pl#cL;D{O>P5T& literal 0 HcmV?d00001 diff --git a/assets/icon/icon_16.png b/assets/icon/icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..f881f426cf658e5c408081ccea10233bad7e614c GIT binary patch literal 263 zcmV+i0r>ujP)Td@^aVdFw{54s2vKiCqJf$&ksKrhKijHh|U z%{{rrBTL_3pEzG=LZ~3YLs4Ec)7+C=S*{48cjjEC4ET>l6=1)j$@#dg6p~vMUnghN+|?E0Gx9WLdY-3 z^BgS8>I96jj{;@R*dZ&5u>EaWrly$^O33c1>th3PEK}B1HJup7_6?xYLQZxkpN0Sc N002ovPDHLkV1m}-Y7_tf literal 0 HcmV?d00001 diff --git a/assets/icon/icon_24.png b/assets/icon/icon_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ac9b95befc48785e698adb53b191b2fd7062e5fc GIT binary patch literal 369 zcmV-%0gnEOP)!zPNY`%Y~OOYfbq1(~iA)!|>tPFKU?2 zAi&1PAj8RN!tmpV42uLCn;A6?2LX^-VDlImrCE4cSlFm(I5J>lU|?iopfO+?wSW*{ zVITqIC@F#I_0OL(JpKNi`WC2gbAxG4W@e)Gj7IVZ2qG3z6c=2+JVbpE6cohp_3KxL zqeqW2@bK_}`FHQ$W!SlMCv|*aZEekP=gu9l!B$pQ44*%LCdzQ4Ef5zMXUNLRVpz9s z9T?xccaNd8w3K>5^z!9PhDD1OF}S(8f!RvXGKm<#=&Y`O{?V5&2DGTvv;_n%FunZw z^Q0^%C)EuH0T~XCr!1hR1>=t&GXEGErJ2ChDCOWk1H*r5PR?ilIXN=`j0tWLl=SHp P00000NkvXXu0mjfRUxA# literal 0 HcmV?d00001 diff --git a/assets/icon/icon_256.png b/assets/icon/icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd78c7e16f60d9e33e0829414d7dcbafb9dd167 GIT binary patch literal 3673 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 literal 0 HcmV?d00001 diff --git a/assets/icon/icon_32.png b/assets/icon/icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..9f270092d19cff905faf4225f3d94099f1c0f7d4 GIT binary patch literal 489 zcmV_uU&*&KtVV^#0&|zu)J1cwcxp01zXQh(G#Nn(-b^LfPM@#z?- z)oK_Hhb(eRxm;#hiN#{md4`fCF|lYg%1qG6Wm!hM-5&WaTOt^IzHKx%&FBqweZD7h z-)uh6Ud;@ad0r)lD=QbDnl^gwCS=>|ZTp?hl8sI>;qXysFi6P#zK^y`Mha_yzT|YO fOFUo7R;y>f;HjV#KR^3^00000NkvXXu0mjffd1k# literal 0 HcmV?d00001 diff --git a/assets/icon/icon_48.png b/assets/icon/icon_48.png new file mode 100644 index 0000000000000000000000000000000000000000..a52db6a6f84179703f762c8ae70b36f5c0c2edfb GIT binary patch literal 759 zcmVF$+n1siToX55Qd!=RVV?(YNla`_!N_x#TJ-Scv;2?yS^ zwFSNv3il@zyS*Lna=AQCtMz&J%*>1ytI=+cTUzd=cXl2nG8t!UeLY@n zY`}=_G&W)`mBLlKT{&sBJ{ekEd{Lv#{9Gy(o!H!z!AO_etz46&?%K<4_se(5Y63WW~i zkw^q%V`B&ggBTqhMPFZ^F%y`cp4Jn{X0upZTPthR%BR1-UoWNJ-d^KwqtSyJ|8tfD zf#1i@T5^?SLKSEwqs??8k2&ICl~NP&dOtl|SviMepsh?LSKw}Q zvq~{leXpH>ABRepkCo^L-EcTAd`zdo%?Yy^Hk0WKofd?M*KSw9Oi+>($!dLC8(Uvr zZ*OYKpB02l8~MCgD3xIJnZN0BsiMUa4$aSx>wl|gUi=$&z`OYi2hWh{j002ovPDHLkV1lWMXEFc) literal 0 HcmV?d00001 diff --git a/assets/icon/icon_512.png b/assets/icon/icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..dc1d577484193a163a335c924e877b086b9faa64 GIT binary patch literal 7669 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 Kv#PN0y7OO|UA|=i literal 0 HcmV?d00001 diff --git a/assets/icon/icon_64.png b/assets/icon/icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..054097c18e6db833a9b5cff24ec0668502355577 GIT binary patch literal 927 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU{3RNaSW-L^K`bihjgGw+keZM zEBkwvhSel2%up%|?AaY95c8uedMX!d>=tgvgsYvha1Xtaq0`kRn9OXstCY;>+-2VAF*=H|2k~b|?6|lCG z`;&P4d(Ht3lOCfT4S!vdjx*1RQI0zMy=raW%$dC*^QT#HT)k@hudeD>mYtODt*+yO z_O`9FKfJQi@)hoNb&da2@;CMTp+hXUw_bT*FE1OJUsjgq#Bet?m3@0r(Y)_^di$=b zNlP)!@9TT!wryLNLWlU&sS|@P=Pa3#%YW|u`)(;4WAWZ;1^X^rKFmFqG~rm9P2b~` zPy5c@{C6Rh^L>1Ws*nfM^^)y6rT?85CQcD-aTefnv|xECfR)i#Cn#PU8~WA#_h#Pb ziEnp0;!_Ma2c&Cnhq`?_lJdTJFS);b`*!I0=arA&zRh(}5?r%p%?h)*uU=(s*|zPF ziSO6i++5z^;NXtbAF6(*UaV*S@cVC8!RyzrFPQMYefyT9)#=o{rr+*vZUI3-i_Sgv zH;_2;;lqaNcg)9*AD=tT_V%@FY^OKP;Y|4T`?qX=`{Bg$XAL*rGcH>)FYooMS4D4f zPH*`$vpCp6K_Ecad-lw^b4!2y*s*O}*vdsacS@c$kzYrwO5;RXY)L>moVTg`Ll z<(KsR{Mq}u?c)8|xH<;KFF$JTC!I`5fBof4IKQ;rn|JH%zx-AS z$pz78?i>2rtX*MYXfw~`. +/// +/// Icon bytes are `include_bytes!()`-embedded at compile time, same +/// shape as the audio assets and default-theme SVGs — no runtime +/// asset-path resolution, no `cargo run` working-directory +/// assumptions. PNG → RGBA decode runs through `tiny_skia` (already +/// in the build for SVG rasterisation), so this system adds zero +/// new dependencies on top of the direct `winit` dep that's +/// already required for `Icon` construction. +#[cfg(not(target_os = "android"))] +fn set_window_icon( + mut applied: Local, + primary_window: Query>, + winit_windows: NonSend, +) { + if *applied { + return; + } + let Ok(primary_entity) = primary_window.single() else { + return; + }; + let Some(window_wrapper) = winit_windows.get_window(primary_entity) else { + // Primary window's underlying winit handle not yet + // populated — `WinitWindows` fills in after the first + // `Resumed` event. Try again next frame. + return; + }; + + // The 256 × 256 PNG is sufficient for `set_window_icon`; winit + // scales it for the actual rendered size. Smaller PNGs in + // `assets/icon/` exist for downstream Linux hicolor / Windows + // `.ico` / macOS `.icns` packaging — they're not used here. + const ICON_BYTES: &[u8] = include_bytes!("../../assets/icon/icon_256.png"); + + let pixmap = match tiny_skia::Pixmap::decode_png(ICON_BYTES) { + Ok(p) => p, + Err(e) => { + eprintln!("warn: could not decode embedded window icon PNG: {e}"); + *applied = true; // don't retry every frame + return; + } + }; + let rgba = pixmap.data().to_vec(); + let icon = match winit::window::Icon::from_rgba(rgba, pixmap.width(), pixmap.height()) { + Ok(i) => i, + Err(e) => { + eprintln!("warn: could not construct window icon: {e}"); + *applied = true; + return; + } + }; + window_wrapper.set_window_icon(Some(icon)); + *applied = true; +} + /// Wraps the default panic hook with one that also appends a crash log /// to `/crash.log` (next to `settings.json`). The default hook /// still runs afterwards, so stderr output and debugger integration are diff --git a/solitaire_engine/examples/icon_generator.rs b/solitaire_engine/examples/icon_generator.rs new file mode 100644 index 0000000..b7a0184 --- /dev/null +++ b/solitaire_engine/examples/icon_generator.rs @@ -0,0 +1,67 @@ +//! Application-icon generator — rasterises the project's icon SVG +//! into `assets/icon/icon_.png` at every size in +//! `card_face_svg::ICON_SIZES` (16, 24, 32, 48, 64, 128, 256, 512, +//! 1024). Sufficient to cover Linux hicolor, Windows `.ico`, and +//! macOS `.icns` packaging targets — and the runtime `Window::icon` +//! wiring picks the 256 px slot. +//! +//! Run with: +//! +//! ```sh +//! cargo run --example icon_generator --release +//! ``` +//! +//! Same shape as `card_face_generator`: SVG builder lives in +//! `solitaire_engine::assets::icon_svg` so the `icon_svg_pin` +//! integration test can call it. Rasterisation runs through +//! `assets::rasterize_svg` (the `usvg` + `resvg` + `tiny_skia` +//! pipeline already used by every other generated asset). + +use bevy::math::UVec2; +use solitaire_engine::assets::icon_svg::{icon_svg, ICON_SIZES}; +use solitaire_engine::assets::rasterize_svg; +use std::path::PathBuf; +use tiny_skia::{IntSize, Pixmap}; + +fn main() { + let icon_dir = workspace_assets_dir().join("icon"); + std::fs::create_dir_all(&icon_dir).expect("create icon dir"); + + let svg = icon_svg(); + + for &size in ICON_SIZES { + let target = UVec2::new(size, size); + let pixmap = rasterize_to_pixmap(&svg, target); + let path = icon_dir.join(format!("icon_{size}.png")); + pixmap + .save_png(&path) + .unwrap_or_else(|e| panic!("write {}: {e}", path.display())); + } + + println!( + "Wrote {} PNGs ({}–{} px) to {}", + ICON_SIZES.len(), + ICON_SIZES.iter().min().copied().unwrap_or(0), + ICON_SIZES.iter().max().copied().unwrap_or(0), + icon_dir.display(), + ); +} + +fn rasterize_to_pixmap(svg: &str, target: UVec2) -> Pixmap { + let image = rasterize_svg(svg.as_bytes(), target).expect("rasterise icon SVG"); + let bytes = image.data.expect("rasterised image carries pixel data"); + debug_assert_eq!( + bytes.len(), + (target.x * target.y * 4) as usize, + "rasterised buffer must match width × height × 4 RGBA bytes", + ); + let size = IntSize::from_wh(target.x, target.y).expect("non-zero target size"); + Pixmap::from_vec(bytes, size).expect("RGBA buffer forms a valid Pixmap") +} + +fn workspace_assets_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("solitaire_engine crate has a workspace-root parent") + .join("assets") +} diff --git a/solitaire_engine/src/assets/icon_svg.rs b/solitaire_engine/src/assets/icon_svg.rs new file mode 100644 index 0000000..8cc1f94 --- /dev/null +++ b/solitaire_engine/src/assets/icon_svg.rs @@ -0,0 +1,70 @@ +//! SVG builder for the Solitaire Quest application icon. +//! +//! Renders the project's signature `▌RS` Terminal mark (the same +//! cursor-block + monogram pair used on the splash boot-screen and +//! card backs) on a dark `#151515` background with a 1 px brick-red +//! border. Square aspect, authored in a 64-unit logical box and +//! scaled at the rasterisation site. +//! +//! Reads at every size from 16 px taskbar tile to 1024 px macOS +//! Retina icon — the high-contrast cursor block carries the +//! recognition load and the smaller `RS` letters sit beneath as +//! a secondary recognition cue. +//! +//! Same SVG-to-PNG pipeline as `card_face_svg` — `icon_generator` +//! example rasterises this at multiple target sizes and writes +//! into `assets/icon/`. The `icon_svg_pin` integration test hashes +//! rasterised RGBA bytes to guard against `usvg`/`resvg` drift. + +use bevy::math::UVec2; + +/// Default rasterisation target — single canonical size used by the +/// runtime `Window::icon` wiring. The generator example emits +/// additional sizes (16, 32, 48, 64, 128, 256, 512, 1024) for the +/// Linux hicolor hierarchy and for downstream `.ico` / `.icns` +/// packaging. +pub const TARGET: UVec2 = UVec2::new(256, 256); + +/// Every size the `icon_generator` example emits. Covers Linux +/// hicolor (16, 24, 32, 48, 64, 128, 256, 512), Windows `.ico` +/// targets (16, 32, 48, 256), and macOS `.icns` targets (16, 32, +/// 64, 128, 256, 512, 1024). +pub const ICON_SIZES: &[u32] = &[16, 24, 32, 48, 64, 128, 256, 512, 1024]; + +const BG: &str = "#151515"; // BG_BASE +const ACCENT: &str = "#a54242"; // ACCENT_PRIMARY brick red +const FG: &str = "#d0d0d0"; // TEXT_PRIMARY + +/// Build the icon SVG. Square aspect, 64 logical units per side. +pub fn icon_svg() -> String { + // Layout in a 64×64 logical box: + // border: 1 logical unit, brick-red, inset 0.5 to + // centre the stroke inside the pixmap. + // corner radius: 6 units (~9 % of side, scales smoothly down + // to 16 px where it disappears into pixel grid). + // `▌` cursor: 18 px tall, 6 px wide, brick-red, centred + // horizontally, sitting on a baseline at y=40 + // so there's room for `RS` beneath it. + // `RS` mark: 14 px FiraMono Bold at y=58, foreground gray, + // letter-spaced for readability at small sizes. + // + // The `▌` glyph is U+258C (LEFT HALF BLOCK) — same character the + // splash and card-back monogram use, rendered upright at icon + // scale. FiraMono carries this at usable size (verified by the + // splash + card-back rendering), so `` is safe here unlike + // the suit glyphs. + format!( + r##" + + + + + + + RS +"## + ) +} diff --git a/solitaire_engine/src/assets/mod.rs b/solitaire_engine/src/assets/mod.rs index 078647b..e016757 100644 --- a/solitaire_engine/src/assets/mod.rs +++ b/solitaire_engine/src/assets/mod.rs @@ -7,6 +7,7 @@ //! `AssetSource` implementations for `embedded://` and `themes://`. pub mod card_face_svg; +pub mod icon_svg; pub mod sources; pub mod svg_loader; pub mod user_dir; diff --git a/solitaire_engine/tests/icon_svg_pin.rs b/solitaire_engine/tests/icon_svg_pin.rs new file mode 100644 index 0000000..8e0bdad --- /dev/null +++ b/solitaire_engine/tests/icon_svg_pin.rs @@ -0,0 +1,114 @@ +//! Pinning test for the application-icon SVG builder. +//! +//! Hashes the raw RGBA8 pixel bytes produced by rasterising +//! `icon_svg()` at every size in `ICON_SIZES`, compares each hash +//! to an embedded constant, and fails on any drift. Catches +//! `usvg`/`resvg`/`tiny_skia` rendering changes and any +//! intentional builder edit that wasn't paired with a hash +//! refresh. +//! +//! When the icon SVG changes intentionally (or a dependency +//! upgrade legitimately changes rendering), update `EXPECTED` by +//! emptying it (`&[]`) and re-running this test once — the test +//! will panic with the new hashes formatted as Rust source ready +//! to paste back in. Same bootstrap pattern as +//! `card_face_svg_pin.rs`. + +use bevy::math::UVec2; +use solitaire_engine::assets::icon_svg::{icon_svg, ICON_SIZES}; +use solitaire_engine::assets::rasterize_svg; + +const EXPECTED: &[(u32, u64)] = &[ + (16, 0x07e641beea430d66), + (24, 0x24e66767f4756a60), + (32, 0xf22a3104623a3873), + (48, 0x2d7f978cf7b12763), + (64, 0x1b377e3e30202eba), + (128, 0xafdc80f901b45518), + (256, 0x82b5b46f73c5921d), + (512, 0xe14c018e1e285209), + (1024, 0xfcd0a6a3beb68bdb), +]; + +#[test] +fn rasterised_icon_bytes_match_pinned_hashes() { + let actual = compute_actual_hashes(); + + if EXPECTED.is_empty() { + panic_with_hashes_to_paste(&actual); + } + + assert_eq!( + actual.len(), + EXPECTED.len(), + "icon-size count drifted (actual {} vs expected {})", + actual.len(), + EXPECTED.len(), + ); + + let mut mismatches: Vec = Vec::new(); + for ((actual_size, actual_hash), (expected_size, expected_hash)) in + actual.iter().zip(EXPECTED.iter()) + { + assert_eq!( + actual_size, expected_size, + "icon-size order drifted", + ); + if actual_hash != expected_hash { + mismatches.push(format!( + " icon_{actual_size}: actual 0x{actual_hash:016x} expected 0x{expected_hash:016x}", + )); + } + } + + if !mismatches.is_empty() { + let mut msg = String::from( + "rasterised icon bytes drifted from EXPECTED — usvg/resvg/tiny_skia/font upgrade?\n", + ); + for m in &mismatches { + msg.push_str(m); + msg.push('\n'); + } + msg.push_str( + "\nIf this drift is intentional, replace EXPECTED with `&[]` and re-run\nthis test to print fresh hashes.\n", + ); + panic!("{msg}"); + } +} + +fn compute_actual_hashes() -> Vec<(u32, u64)> { + let svg = icon_svg(); + ICON_SIZES + .iter() + .map(|&size| (size, hash_rasterised(&svg, size))) + .collect() +} + +fn hash_rasterised(svg: &str, size: u32) -> u64 { + let target = UVec2::new(size, size); + let image = rasterize_svg(svg.as_bytes(), target).expect("rasterise icon SVG"); + let bytes = image.data.expect("rasterised image carries RGBA pixel data"); + fnv1a(&bytes) +} + +/// FNV-1a 64-bit, inline. Same shape as `card_face_svg_pin.rs` — +/// no cryptographic strength needed, just stable byte fingerprints. +fn fnv1a(bytes: &[u8]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for &b in bytes { + h ^= b as u64; + h = h.wrapping_mul(0x0000_0100_0000_01b3); + } + h +} + +fn panic_with_hashes_to_paste(actual: &[(u32, u64)]) -> ! { + let mut out = String::from( + "\nEXPECTED is empty — paste the following into the const literal:\n\nconst EXPECTED: &[(u32, u64)] = &[\n", + ); + for (size, hash) in actual { + out.push_str(&format!(" ({size}, 0x{hash:016x}),\n")); + } + out.push_str("];\n"); + panic!("{out}"); +}