Skip to content

fix(tray): monochrome template icon in the macOS menu bar - #19

Open
maerlin wants to merge 1 commit into
karem505:masterfrom
maerlin:fix/macos-tray-template
Open

fix(tray): monochrome template icon in the macOS menu bar#19
maerlin wants to merge 1 commit into
karem505:masterfrom
maerlin:fix/macos-tray-template

Conversation

@maerlin

@maerlin maerlin commented Aug 8, 2026

Copy link
Copy Markdown

Problem

The tray icon is the full-colour green app mark on every platform. On macOS that makes it the one item in the menu bar that does not follow the system appearance: AppKit expects a template image, a silhouette in the alpha channel that it tints black on a light menu bar and white on a dark one. A colour icon stays green either way and reads as foreign next to every other status item.

Windows and Linux tray areas have no equivalent convention, so they are untouched.

Change

  • A macOS-only pair of assets, icons/tray-macos-template.png and -unread.png, selected with #[cfg(target_os = "macos")]. Everything else keeps tray.png / tray-unread.png.
  • TrayIconBuilder::icon_as_template(ICON_IS_TEMPLATE), where the constant is cfg!(target_os = "macos").
  • Badge updates go through set_icon_with_as_template instead of set_icon. Setting the image and the template flag separately renders the icon twice and visibly flickers; Tauri documents the atomic call for exactly this, and it falls back to set_icon on Linux and Windows.
  • The unread variant scales the glyph to 84% and puts a badge dot in the freed corner, ringed by a transparent gap. A template image has no second colour to spend, so the gap is what keeps the dot readable as a separate mark once AppKit flattens everything to one colour.

How the assets are generated

scripts/make-tray-template.mjs, wired into regen-icons.sh after the existing tray step. It reads the 512px render of icons/app-icon.svg, so the SVG stays the single source of truth for every icon in the repo.

It recovers the silhouette from the raster rather than the path data: flood-fill the non-white pixels inward from the image border, and whatever the fill cannot reach is the bubble, walled off by its own white stroke. The handset is then the white blob that is not that stroke ring. Output is 36x36 (18pt @2x), which is the exact height tray-icon renders a status-item image at, so the bitmap lands 1:1 on a Retina menu bar.

Zero dependencies, PNG in and out via node:zlib, matching the other scripts here. Regenerating the template does not need ImageMagick.

Testing

  • cargo build, cargo test (66 pass), cargo fmt --check clean.
  • Built a release bundle and ran it on macOS 15 (Apple Silicon). The icon renders as a monochrome silhouette in the menu bar, matching neighbouring status items, and inverts with the system appearance.
  • Linux and Windows paths are compile-time excluded and unchanged; CI's three-platform build covers them.

Happy to attach a before/after menu-bar screenshot if useful.

The green app mark was used verbatim as the tray icon on every platform. On macOS that is the one thing in the menu bar that does not follow the system: AppKit expects a template image (a silhouette in the alpha channel that it tints black on a light menu bar and white on a dark one). A full-colour icon therefore stays green regardless of appearance and reads as foreign next to every other status item.

Ship a macOS-only pair of template assets (plain + unread) and set icon_as_template, keeping the colour icon on Windows and Linux, whose tray areas have no equivalent convention. Badge updates go through set_icon_with_as_template so the image and the template flag are applied in one step; setting them separately renders the icon twice and flickers.

The assets are generated by scripts/make-tray-template.mjs (wired into regen-icons.sh) from the 512px render of icons/app-icon.svg, so the SVG stays the single source of truth. It recovers the silhouette from the raster rather than the path data: flood-fill inward from the border, and whatever the fill cannot reach is the bubble, walled off by its own white stroke; the handset is then the white blob that is not that stroke. The script has zero dependencies (PNG in/out via node:zlib, like the other scripts here), so regenerating the template does not need ImageMagick. Output is 36x36 (18pt @2x), the exact height tray-icon renders a status item at.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant