1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! macOS dock-icon override.
//!
//! In a packaged app the dock icon comes from the bundle: `Info.plist`'s `CFBundleIconFile` ->
//! `Resources/icon.icns`. macOS composites that through the system icon mask, so it is already
//! correct — and it is on screen from the moment the app bounces, before any Rust runs.
//!
//! Tauri overrides it at Ready under `#[cfg(all(dev, target_os = "macos"))]` (see `tauri::app`) —
//! and the `dev` cfg is emitted for plain-cargo builds, so this happens in PACKAGED apps too, not
//! just `tauri dev`. The crate's embedded icon is a transparent placeholder, so whatever Tauri
//! sets at Ready is invisible: EVERY build must reassert at Ready or the dock goes blank.
//!
//! Tauri's override runs on `RuntimeRunEvent::Ready`, so `reassert_on_ready` -- called from the
//! host's run-event handler for `RunEvent::Ready` -- lands immediately after it, deterministically.
//! The timed schedule alone could not: `Ready` fires when the event loop is up, which on a slow dev
//! boot is well past the last 2000 ms tick, and the host's icon would show first and then be
//! replaced by Tauri's embedded one.
//!
//! The pre-Ready timed schedule stays dev-only: there the bundle's system-masked `.icns` is
//! already on screen and correct, and replacing it early was the original "icon flash".
//! `setApplicationIconImage:` draws the NSImage RAW,
//! with no system mask, so a host whose `RunConfig.icon` is a full-bleed square (the common case —
//! it doubles as the tray image) had its correctly-masked bundle icon replaced by a hard-cornered
//! square a beat after launch. That swap is the "icon flash".
//!
//! The tray icon is separate, via Tauri's `TrayIconBuilder::icon` — this is only the dock. objc2,
//! so apple + macos only.
use RefCell;
use Retained;
use AnyThread;
use ;
use ;
// The last icon this module applied: (source path, the exact NSImage instance set). Main-thread
// only (set_dock_icon bails off-main), so a thread_local RefCell suffices — no locks.
//
// Why this exists: on macOS 26 every `setApplicationIconImage:` with a NEW NSImage instance makes
// AppKit composite a fresh dock tile (multi-appearance, f16, GPU-backed) that is cached by image
// identity and never evicted — ~30MB of IOAccelerator memory pinned per call. A host that
// re-asserts its icon on a timer therefore leaks unboundedly. Skipping the set when our image is
// still the one installed makes repeated re-asserts free.
thread_local!
/// Whether the running process is a packaged `.app` whose `Info.plist` declares an icon.
///
/// This is the real question — NOT whether this is a debug or release build. `tauri::is_dev()` is
/// `!cfg!(feature = "custom-protocol")`, and a tish host builds with plain cargo rather than the
/// Tauri CLI that would set that feature, so it reports `true` even in a shipped bundle. Asking
/// the bundle directly works no matter how the binary was compiled or launched.
/// Set the process's dock icon from a PNG (or any NSImage-readable) file. Returns whether an
/// image was actually loaded and set — callers with a fallback must know. No-op off the main
/// thread or if the image can't be loaded (best-effort branding, never fatal).
///
/// Idempotent: when the requested path is the one already applied AND the application icon still
/// points at the exact NSImage instance we set, this returns true without decoding or setting
/// anything. The pointer compare (not the path compare alone) is load-bearing — if something
/// replaced the icon out from under us (Tauri's Ready override, AppKit), `applicationIconImage`
/// no longer returns our instance and we re-apply, so restore-after-clobber semantics survive.
/// The bundle's own icon file (`Info.plist` `CFBundleIconFile` under `Contents/Resources`),
/// tolerating the extensionless convention. None for a loose binary or an iconless bundle.
/// Reassert the host's dock icon right after Tauri sets its own.
///
/// Tauri's `#[cfg(dev)]` override happens on `RuntimeRunEvent::Ready`
/// (`tauri::app::on_event_loop_event`), which the host observes as `RunEvent::Ready` — and the
/// `dev` cfg is emitted for plain-cargo builds, so it fires in PACKAGED apps too, not just dev
/// runs. Since the crate's embedded icon is a transparent placeholder, whatever Tauri set at
/// Ready is invisible: a packaged app that declines to reassert here ships a BLANK dock icon.
/// That is exactly what v1.3.x did in production when this function still returned early for
/// bundles.
///
/// So: unconditional, and never allowed to leave the transparent image standing. If the host's
/// own icon fails to load, fall back to the bundle's `.icns` (`CFBundleIconFile`), which macOS
/// showed from launch until Ready.
///
/// Must be called on the main thread — the run-event handler already is.
/// Apply the host's dock icon, in dev only.
///
/// Under `tauri dev` Tauri sets its own embedded icon during startup, after `setup()` runs, so a
/// single early call loses the race. This covers the window before `Ready` (and any later AppKit
/// reset); `reassert_on_ready` is what guarantees the host icon wins Tauri's own set.
///
/// In a release build this is a no-op: the bundle's `.icns` is already the right icon, already
/// system-masked, and already on screen. Overwriting it with a raw NSImage is what made the icon
/// visibly change shortly after launch.