Skip to content

Commit eaefa9a

Browse files
authored
fix(ui): use a template tray icon on macOS (#202)
Makes the macOS menu-bar icon a proper **template** image, so the OS tints it for light/dark/selected like every other menu-bar extra instead of it sitting there as the one coloured blob. **Verified visually on macOS 26 hardware** (see Verification). Scope note: this PR originally also carried a Dock-icon change. That half was **dropped** on review because it could not be demonstrated - the findings are preserved as documentation instead (DESIGN 8.1.1). See "Dock icon" below. ## The config was a red herring `tauri.conf.json` already had `trayIcon.iconAsTemplate: true`, but that never reaches the live icon: `tray.rs::build()` calls `remove_tray_by_id("main")` and replaces the config-defined tray with a runtime-generated `"driven-main"` one, built from `include_bytes!("icons/64x64.png")` (a white glyph on **opaque teal**) with `set_icon_as_template(false)` forced at four sites - including one inside the spinner animation task, which is the easy one to miss. **Flipping that flag would have made it worse, not better.** macOS template images are alpha-only: AppKit discards the RGB and tints every pixel by its alpha. Handing it the opaque brand PNG renders a **solid black square**. ## What this does instead - **New asset** `src-tauri/icons/tray-template.png` - 64x64, black on transparency. Rasterised from the already-committed `icons/tray.svg` (which was always monochrome template art) with a throwaway supersampling script; it is just the union of that SVG's 4 circles, 1 rect and 1 polygon, so it can be regenerated from the SVG with any rasteriser. - **`TrayIcon::template_rgba_frame`** paints in *alpha*: the badge's contrast band is punched fully transparent (a template has no second ink, so a hole is the only way to separate the badge from the mark behind it), and each state's glyph is cut **out** of the badge disc. - The holes are cut with the **same glyph painters as the colour path** - rendered into a scratch buffer and inverted into alpha - so the platforms cannot drift apart in shape, and the spinner's comet-tail fade (and therefore its frame-to-frame distinctness) survives. - `set_template_mode()` replaces all four hardcoded `false` calls, so the flag can never disagree with the pixels just handed over. If the template PNG ever fails to decode it falls back to the colour path with template **off** - never "opaque pixels declared as a template". - Windows/Linux are unchanged. ## Deliberate divergence from DESIGN 8.1 - please sanity-check **On macOS the tray state is now carried by SHAPE only; there is no yellow or red.** A template cannot express colour. This is sound because the badge glyphs (spinner / pause bars / `!` / `X`) were already designed to be readable without colour, and it is what `SPEC.md:1579`'s `iconAsTemplate` for macOS asks for - but it does contradict DESIGN 8.1's colour table, so `design/DESIGN.md` now carries an explicit macOS carve-out telling future readers not to revert it. The template badge is drawn larger than the colour one (`BADGE_R_TEMPLATE` 0.28 vs `BADGE_R_COLOUR` 0.24, centre pulled in to 0.66) so the punched holes survive being scaled to a 22pt menu bar. An earlier attempt at 0.32 **overflowed the 64px canvas and clipped flat against the edges** - caught by rendering the states and looking at them, and now guarded by a `template_badge_fits_in_the_canvas` test. ## Dock icon: documented, not fixed (DESIGN 8.1.1) `cargo tauri dev` shows the generic green "exec" icon. This is a **dev-run cosmetic artifact, not a shipped-product defect**, and after investigation it is deliberately not fixed in code: - **Release bundles were never affected** - `icons/icon.icns` is in `bundle.icon` and is a valid 1024px multi-rep icns, so `Driven.app` has always had the right Dock icon. - **Tauri already performs the only available runtime fix**: the icns is genuinely embedded in the dev binary (verified by byte-searching it) and `setApplicationIconImage` is already called at `RunEvent::Ready`. Re-applying it is a re-run of a mechanism that is already executing. - **That API cannot fix the surface most likely being seen anyway**: status bars / window switchers read `NSRunningApplication.icon()`, which returns the static **LaunchServices file-type** icon. Confirmed with a control experiment - a bundle-less process that successfully sets its own icon still reports the generic one through that API. - **The only real fix for an unbundled run is to run a bundled `.app`.** All of the above is now written down in `DESIGN.md` 8.1.1 so nobody re-derives it. If the generic icon is ever reported against a *release* build, that is a different bug with different evidence. ## Verification **Confirmed visually on hardware.** Built and ran the app, then captured the real menu bar: the Driven icon (cloud + up-chevron) renders in exactly the same single-colour tint and weight as its neighbouring template icons - the reported coloured blob is gone. All five states were additionally rendered to PNG and inspected at **22px and 44px** in simulated **light and dark** menu bars: idle / spinner / pause-bars / `!` / `X` stay mutually distinguishable by shape at menu-bar size. ## Gates - `cargo clippy --workspace --all-targets -- -D warnings` - clean - `cargo fmt --all -- --check` - clean - `SQLX_OFFLINE=true cargo test --workspace` - green (37 suites, 0 failures) - No `ui/` files touched, so the pnpm gates do not apply. - Diff is 3 files: `tray.rs`, the new PNG, and `DESIGN.md`. No dependency or lockfile changes. New tests: seven macOS-gated tray tests asserting on the **alpha** channel (the only channel macOS reads), including `template_source_is_alpha_shaped` as the explicit guard against the solid-black-square regression.
1 parent 18b0d43 commit eaefa9a

3 files changed

Lines changed: 477 additions & 45 deletions

File tree

design/DESIGN.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,59 @@ Always-present icon. States:
13981398
| Red | Error state requires attention (auth needed, decrypt failure, disk full) |
13991399

14001400
The yellow-with-`!` state covers all of §5.8's network failure modes.
1401+
1402+
**macOS carve-out - the colours above are Windows/Linux only.** macOS
1403+
menu-bar extras are expected to be *template* images: alpha-only art the
1404+
OS tints itself (black on a light menu bar, white on a dark or selected
1405+
one). A colour-bearing icon there does not read as themed, it reads as
1406+
broken - it is the one coloured blob among the system's monochrome
1407+
extras. So on macOS the tray renders a template mark and **state is
1408+
carried by the badge SHAPE alone**: spinner = syncing, pause bars =
1409+
paused, `!` = network attention, `X` = error, plain mark = idle. That is
1410+
the same shape vocabulary as the table above, minus the colour; the
1411+
glyphs were always designed to be readable without colour (colour-blind
1412+
users, tiny trays), which is what makes dropping it safe here. macOS
1413+
therefore has no yellow or red tray icon - every other mention of a
1414+
"yellow"/"red" tray icon in this document (§5.8.1, §5.10.2) means the
1415+
shape-equivalent state there. This is deliberate and matches SPEC
1416+
§1579's `iconAsTemplate` for macOS; do **not** "fix" the macOS path back
1417+
to a colour icon.
1418+
1419+
#### 8.1.1 macOS Dock icon on `cargo tauri dev` (known, dev-only, WONTFIX)
1420+
1421+
**A `cargo tauri dev` run shows the generic green "exec" icon instead of
1422+
the Driven mark. This is a dev-run cosmetic artifact, not a defect in the
1423+
shipped product** - do not spend time "fixing" it in the app, and do not
1424+
treat a report of it against a dev build as a release bug.
1425+
1426+
Why: `cargo tauri dev` launches the bare `driven-app` Mach-O with no
1427+
`.app` wrapper, so LaunchServices has no icon to associate with the
1428+
process. A packaged build is unaffected - `icons/icon.icns` is listed in
1429+
`bundle.icon` in `tauri.conf.json` and ends up in `Contents/Resources`,
1430+
so `Driven.app` has always had the correct Dock icon.
1431+
1432+
Investigated 2026-07 and deliberately NOT fixed in code. The findings,
1433+
so nobody re-derives them:
1434+
1435+
- **Tauri already does the only available runtime fix.** `tauri-codegen`
1436+
embeds the icns whenever `target == MacOS && dev` (confirmed: the icns
1437+
bytes are present verbatim in the built dev binary), and `tauri`
1438+
calls `-[NSApplication setApplicationIconImage:]` at `RunEvent::Ready`.
1439+
Re-applying it later is a re-run of a mechanism already executing.
1440+
- **`setApplicationIconImage:` cannot fix the surface people usually
1441+
notice.** Third-party status bars, window switchers, and anything else
1442+
reading `NSRunningApplication.icon()` get the **static LaunchServices
1443+
file-type icon**, which that call does not affect. Confirmed with a
1444+
control experiment: a bundle-less process that successfully sets its
1445+
own icon still reports the generic one through that API.
1446+
- **So the only real fix for an unbundled run is to run a bundled
1447+
`.app`** (e.g. `cargo tauri build` and launch the bundle), not any
1448+
runtime API call.
1449+
1450+
If the generic icon is ever reported against a RELEASE build, that is a
1451+
different bug with different evidence (a broken/missing icns in the
1452+
bundle) - chase it then, and do not assume it is this.
1453+
14011454
The tooltip shows the specific condition ("Connected, no Internet",
14021455
"Captive portal — click to sign in", "Google Drive is unavailable", etc.)
14031456
and the tray menu surfaces the matching action ("Open captive portal",

src-tauri/icons/tray-template.png

414 Bytes
Loading

0 commit comments

Comments
 (0)