Documentation

App Icon

Point icon at a PNG and Whitehall ships it. Leave it out and the app gets the default icon — a real adaptive icon built from the project name, not a stand-in.

Using your own

One line in whitehall.toml:

[app]
icon = "static/icon.png"

512×512 or larger, PNG. Whitehall resizes it into every density bucket (mipmap-mdpi through mipmap-xxxhdpi) at build time.

Icon variants

An app can ship more than one icon and switch between them at runtime — a dark variant, a seasonal one:

[app.icons]
default = "static/icon.png"
dark = "static/icon-dark.png"

Each name becomes an ic_launcher_<name> resource, with default taking the plain ic_launcher slot.

Switching at runtime

Three functions read and change which variant is active:

val current = $getIcon()      // "default", "dark", …
val available = $getIconList() // ["default", "dark"]

$setIcon("dark")
$setIcon works by enabling and disabling manifest activity aliases, which is the only mechanism Android offers. Two consequences worth knowing before you build a feature on it: the app is killed when the switch takes effect, and some launchers cache the old icon until they are restarted. It suits a setting the user chooses deliberately, not something that follows the system theme.

The default icon

With no icon set, the app gets the Whitehall ghost knocked out of a field of colored light. The same project name always produces the same icon, on every machine and every release — the color comes from a hash of the name, not from a random seed or a timestamp.

What stays the same, and what doesn't

Two things are fixed in every default icon, so a shelf of Whitehall apps reads as one set:

  • The ghost. Same silhouette, same size, same position.
  • Salmon eyes. #F05758, the color from the Whitehall logo, in every icon. It sits in the eyes rather than in the background because a soft-edged light blends with whatever is under it, and blending salmon into an app's own hue turns both to mud.

What varies is the field behind it — its hue, and the size and placement of three lights.

Why the colors look like that

Every color is picked by searching the sRGB gamut boundary in OKLCH for the most saturated value that survives at a given lightness. Choosing a saturation up front and clipping whatever falls outside is what makes generated palettes look muddy: the clip eats far more of some hues than others, so a yellow screams while a blue goes grey. Searching for the boundary instead means every hue comes out equally loud.

Two rules keep a field from going flat or chalky:

  • Every light stays between L 0.46 and L 0.72, where sRGB holds the most chroma. Reaching higher for a bright highlight climbs into the narrow end of the gamut and takes the whole field pastel with it.
  • The hues within one icon stay inside a 75° span. Blending two saturated colors from opposite sides of the wheel passes through grey, and a soft-edged light is mostly blend.

Apps get the 280° of the hue wheel that isn't near the brand salmon. The warm quarter is reserved, which is what keeps the eyes legible against every field.

Layers

The generator writes a full adaptive icon, not a flat bitmap:

ResourceWhat it is
mipmap-anydpi-v26/ic_launcher.xmlThe adaptive icon
ic_launcher_background.pngFlat near-black, which the ghost-shaped hole reveals
ic_launcher_foreground.pngThe field, with the ghost punched out of it
ic_launcher_monochrome.pngThe bare silhouette, for Android 13+ themed icons
mipmap-*/ic_launcher.pngA pre-masked circle for API 25 and below
The adaptive icon is why the artwork runs corner to corner. Ship a bare bitmap as ic_launcher and Android 8+ treats it as legacy: it shrinks the image to roughly two thirds, drops it on a white plate, then masks that to the launcher's shape. That white ring around an icon is Android's, not yours — and it happens to hand-made icons just as readily as the default one.

When two apps come out alike

Hue is drawn independently per app, so nothing stops two names from landing a degree apart. A second axis taken from unrelated bits of the hash varies how deep the field runs, which separates most collisions into a pale icon and a dark one. If two of your apps still look too close, give one of them an icon of its own.

Notification icon

Status bar icons are a separate, stricter format — Android draws them as a silhouette, so anything but a flat white shape on transparent comes out as a white blob:

[app]
notification_icon = "static/notification-icon.png"

96×96 or larger. This becomes R.drawable.ic_notification, which is what $notify reaches for when a notification doesn't name an icon of its own — so an app that posts notifications needs either this line or an icon= on every one of them. Unlike the app icon, there is no default.