{"record":{"id":"b9343e48e5e7ce31","repo":"slint-ui/slint","slug":"systemtrayicon-must-be-created-on-the-main-thread","errorCode":null,"errorMessage":"SystemTrayIcon must be created on the main thread on macOS","messagePattern":"SystemTrayIcon must be created on the main thread on macOS","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/core/items/system_tray/appkit.rs","lineNumber":203,"sourceCode":"// ---------------------------------------------------------------------------\n// PlatformTray: one per SystemTrayIcon item.\n// ---------------------------------------------------------------------------\n\npub struct PlatformTray {\n    status_item: Retained<NSStatusItem>,\n    action_target: Retained<MenuAction>,\n    appearance_observer: Retained<AppearanceObserver>,\n    mtm: MainThreadMarker,\n}\n\nimpl PlatformTray {\n    pub fn new(\n        params: Params,\n        self_weak: ItemWeak,\n        _context: &crate::SlintContext,\n    ) -> Result<Self, Error> {\n        let mtm = MainThreadMarker::new()\n            .expect(\"SystemTrayIcon must be created on the main thread on macOS\");\n\n        let image = image_to_nsimage(params.icon)?;\n\n        let status_bar = NSStatusBar::systemStatusBar();\n        let status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength);\n\n        let action_target = MenuAction::new(mtm, self_weak.clone());\n\n        if let Some(button) = status_item.button(mtm) {\n            button.setImage(Some(&image));\n            let tooltip = NSString::from_str(params.tooltip);\n            button.setToolTip(Some(&tooltip));\n            // Slint's `title` is the visible text next to the icon (think\n            // battery percentage or system clock). Setting an empty string\n            // simply leaves no label, which is the natural default.\n            let title = NSString::from_str(params.title);\n            button.setTitle(&title);\n            // Route clicks back to slint's `clicked` callback. NSStatusItem","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/slint-ui/slint/blob/3fd8f2ec03c2aa8a95d5f4b9daa299c7b8bf4016/internal/core/items/system_tray/appkit.rs#L185-L221","documentation":"On macOS the system tray is implemented with AppKit (NSStatusItem), and AppKit objects may only be touched from the process's main thread. objc2's MainThreadMarker::new() returns None when called off the main thread, so PlatformTray::new() panics via .expect('SystemTrayIcon must be created on the main thread on macOS') the moment a SystemTrayIcon is created anywhere else. This is a hard platform constraint, not a transient error.","triggerScenarios":"Instantiating a component containing a SystemTrayIcon (or otherwise creating the tray) inside std::thread::spawn, a tokio worker thread, or any background thread on macOS; an embedding (Python/Node/host app) initializing the Slint UI on a side thread instead of the one that runs the event loop.","commonSituations":"Porting from Linux/Windows where tray creation in a worker thread appeared to work; apps that spawn helper threads at startup and let tray setup land on one of them; test harnesses that construct components off the main thread; CI running macOS runners with multi-threaded bootstrap code.","solutions":["Create the SystemTrayIcon on the main thread - before slint::run_event_loop() is started there, or from a closure dispatched to the event loop thread (slint::invoke_from_event_loop).","Keep all UI construction (windows, tray, component instantiation) on the same thread that runs run_event_loop().","Restructure startup so worker threads are spawned after tray/window creation, not around it.","If the embedding forces off-main init, move only data work to threads and hand UI handles back to main."],"exampleFix":"// before: component (with a SystemTrayIcon element) created off the main thread\nstd::thread::spawn(|| {\n    let ui = App::new().unwrap(); // PANIC on macOS: AppKit is main-thread-only\n});\n\n// after: instantiate on the main thread that also runs the event loop\nfn main() {\n    let ui = App::new().unwrap(); // main thread: tray creation succeeds\n    ui.run().unwrap();            // event loop stays on that same thread\n}","handlingStrategy":"validation","validationCode":"#[cfg(target_os = \"macos\")]\nfn on_main_thread() -> bool {\n    // mirrors what objc2's MainThreadMarker checks\n    unsafe { libc::pthread_main_np() != 0 }\n}\n\n#[cfg(target_os = \"macos\")]\nassert!(on_main_thread(), \"create the SystemTrayIcon on the main thread\");\nlet ui = App::new().unwrap(); // component containing the tray element","typeGuard":null,"tryCatchPattern":"// Catching the panic does NOT make AppKit usable off-main;\n// only use this to log and retry creation on the main thread:\nlet attempt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    create_tray_here()\n}));\nif attempt.is_err() {\n    slint::invoke_from_event_loop(create_tray_here).ok(); // re-create on main\n}","preventionTips":["Create all UI (windows, tray, component instances) on the thread that will run slint::run_event_loop","On macOS assume every AppKit-backed object is main-thread-only","Put tray setup at the top of main() before spawning any worker threads","Add a startup debug_assert on thread identity on macOS"],"tags":["macos","system-tray","main-thread","appkit","panic"],"backgroundTag":"main-thread-only-api","analyzedSha":"3fd8f2ec03c2aa8a95d5f4b9daa299c7b8bf4016","analyzedAt":"2026-08-19T23:23:16.610Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}