lys-ui icons
How lys-ui resolves and paints themed icons.
Pieces
internal/icons— the freedesktop icon theme spec in pure Go:index.themeparsing (sections,Inherits,Directories/ScaledDirectories, per-directorySize/Type/MinSize/MaxSize/Threshold/Scale), the spec's lookup algorithm (exact-size pass, nearest-size pass, inheritance chain with hicolor last, unthemed base-directory fallback),-symbolicname fallback, and a raster cache keyed by (name, logical size, 120-based scale). The last tier is a bundled set of Lucide icons (lucide-static, ISC,internal/icons/lucide): freedesktop names such asdocument-save-symbolicmap onto them, so stock chrome draws on a host with no icon theme, and an installed theme always wins.widget.Icon— five constructors:NewIcon(a raster the caller made),NewThemeIcon(theme name),NewThemeIconFallback(theme name, with fallbacks),NewFileIcon(png / svg / svgz path),NewSVGIcon(embedded bytes, for apps that ship their own glyphs). Icons rasterize at the canvas device scale, so a fractional rescale re-rasterizes crisply.widget.ThemeIconExistsasks whether a theme name resolves,widget.SetIconSearchPathsreplaces the base directories (GTK'sIconTheme.set_search_path; nil restores the XDG default), andwidget.RefreshIconsforgets every resolved icon so icons installed or removed since are picked up.widget.SetIconNamespacenames an application's own icons by prefix and directory, so drawing them never lists the theme chain.render.Icon.Tint— the recoloring primitive.
Symbolic recoloring
Symbolic sources are -symbolic theme files and SVGs painting with currentColor (oksvg rejects currentColor in strict mode, so internal/icons rewrites it to opaque white before parsing). Recoloring is a mask pass: the source is decoded once, and the tinted variant keeps every pixel's alpha while taking the tint's color. This is what GTK does for symbolic icons, and it survives opacity exactly (anti-aliasing and fill-opacity carry into the result).
The honest limits of the mask approach: ink comes out one flat color. Gradient hue variation collapses to its alpha ramp, and multi-color art goes monochrome — which is the symbolic-icon contract, not a bug. Gradients that encode their shape in alpha (the common symbolic idiom, currentColor → transparent) survive unchanged.
A symbolic widget.Icon takes the stylesheet's color, else widget.Current().Accent; SetTint pins a different color (zero un-pins). Non-symbolic sources keep their colors unless a tint is pinned.
Icon-theme changes are followed live
The portal's org.gnome.desktop.interface icon-theme setting is watched by the same monitor that carries the color-scheme preference (internal/portalsettings). The application wires the two together: Monitor.OnIconThemeChange feeds icons.Default().ApplyIconTheme, and an Application does that wiring for you — a desktop theme switch swaps the resolution theme, drops every cached raster, and bumps Generation:
widget.Iconcompares its resolved generation against the cache in its damage drain, so the next frame after a switch repaints the new theme's icons; the application requests that repaint through the loop queue.- An emptied setting keeps the previous theme (Warn, the degraded-but-running convention) — a broken portal never blanks the UI.
- Inside the toolkit,
icons.Cache.OnIconThemeChangedhears about every applied switch, on the monitor's goroutine.
This is a lookup refresh, never a palette swap: the "toolkit never flips its own theme" rule stands, and the color-scheme preference stays the app's to wire (appearance.md).