Baking PNGs was the wrong call and it showed twice over. Reactor's `.icon()` builds a BitmapIcon with `ShowAsMonochrome(false)` and no size, so a baked mark is stuck at ONE colour on every theme and renders at its source pixel count read as DIPs — a colour that never matches the theme, and a size each control template clamps or does not. Draw Lucide's own icon font instead. A FontIcon is sized by the control and tinted from the foreground brush, exactly as the SymbolIcon it replaced was: white on an accent button, the theme's colour in the sidebar, and a vector at any DPI. The ring's discs take the same glyph through a TextBlock, where the size is the ring's geometry and the ink is white because a disc is dark on both themes. Three PNG bakes, the two-colour split, the LOCALAPPDATA materializer and its cache all go. What replaces them is a codepoint per icon in the shared table and one 664 kB font. The font reaches the exe by two routes, because ms-appx:/// resolves to the install folder either way: build.rs stages it for a dev build, and it is checked in under packaging/assets for a shipped one, which pack-msix.ps1 copies into the layout the MSIX, the installer and the portable zip are all packed from. That script now fails loudly if it is missing — every icon would otherwise ship as a private-use box.
Lucide icon masters
The canonical UI marks every client draws its own icons from — the quick-action ring's slots, and the ordinary shell chrome (back, refresh, save, delete…) on the desktop clients.
Lucide v0.462.0, ISC licensed (see THIRD-PARTY-NOTICES.txt), fetched
unmodified from lucide-icons/lucide at that tag. One file per icon, its own name. Every master
is a 24×24 viewBox, fill="none", stroke="currentColor", stroke-width="2", round caps and
joins — Lucide's own drawing contract, and what every derivative below reproduces.
Which client consumes what
scripts/gen-lucide-assets.sh derives all of it. Nothing here is hand-edited.
| client | form | where |
|---|---|---|
| Skia console (gamepad UI) | folded path string, stroked by Skia | crates/pf-client-core/src/lucide.rs → crates/pf-console-ui/src/icons.rs |
| GTK shell | the same path string, stroked by gsk::Path |
crates/pf-client-core/src/lucide.rs |
| WinUI shell | PNG, baked twice (grey and white) | clients/windows/assets/lucide/ |
The two Rust consumers share one table, so a mark cannot differ between the console and the
GTK shell. The WinUI shell bakes because windows-reactor has no vector element: its Image takes
a raster URI, and its BitmapIcon is created with ShowAsMonochrome(false), so a WinUI icon
cannot be tinted at runtime and has to ship in the colour it will be drawn in. Hence two bakes —
lucide/ in mid-grey for ordinary surfaces, lucide-on/ in white for accent buttons and the
ring's dark discs.
Adding an icon
- Drop the master here:
curl -o assets/lucide/<name>.svg https://raw.githubusercontent.com/lucide-icons/lucide/0.462.0/icons/<name>.svg bash scripts/gen-lucide-assets.sh- Add the PNG to
clients/windows/src/app/lucide.rs'sICONStable — the shipped-token list, the same discipline the OS marks and launcher marks keep.
The console's icons.rs and the GTK shell need no list of their own: both read
pf_client_core::lucide, which the script regenerates whole.