Skip to content

fix(macos): pop up status item menu via NSStatusItem (fixes #251)#318

Open
TuYv wants to merge 1 commit into
tauri-apps:devfrom
TuYv:fix/macos-menu-popup-fullscreen-spaces
Open

fix(macos): pop up status item menu via NSStatusItem (fixes #251)#318
TuYv wants to merge 1 commit into
tauri-apps:devfrom
TuYv:fix/macos-menu-popup-fullscreen-spaces

Conversation

@TuYv

@TuYv TuYv commented May 12, 2026

Copy link
Copy Markdown

fix(macos): pop up status item menu via NSStatusItem (fixes #251)

What

Stops the tray menu from silently failing to open on macOS when the click happens on a secondary display while a full-screen Space is active there. Drives the popup directly through NSStatusItem.popUpStatusItemMenu instead of synthesising a button click.

Symptoms (matches #251)

Reported in #251, confirmed across multiple users:

Where the click happens Result
Primary display, desktop Space ✅ menu pops up
Primary display, in a full-screen Space ✅ menu pops up
Secondary display, desktop Space ✅ menu pops up
Secondary display, in a full-screen Space ❌ click is registered but menu never appears

The user comment tauri-apps/tray-icon#251 (comment by @andrewdavidmackenzie) captures this matrix exactly. macOS 14/15, both Intel and Apple Silicon, with and without "Displays have separate Spaces".

Root cause

The current implementation uses button.performClick(None) to trigger the popup (in both show_menu() at src/platform_impl/macos/mod.rs:258 and on_tray_click at src/platform_impl/macos/mod.rs:515).

performClick: simulates a press/release on the button without producing or attaching an NSEvent. NSStatusItem's internal popup logic then resolves the active screen/Space via [NSApp currentEvent]. On a secondary display whose active Space hosts a full-screen application, the most recent NSEvent in scope is bound to a different NSWindow (the full-screen window) than the status bar that received the real click; AppKit's popup positioning resolves to the wrong context and no-ops without warning or log output.

This regression was introduced in PR #69 refactor(macos): rewrite impl to fix missing click issues (commit 54fc7de, 2023-08-03, first released in tray-icon@0.7.5). Before that PR, clicks reached the NSStatusBarButton directly via sendActionOn: and AppKit drove the popup with the real NSEvent. After the PR, a custom TrayTarget subview intercepts mouseDown: to surface Click/Enter/Move events to the Rust caller — so the real event is consumed by the subview and performClick(None) is the only thing left to drive the popup.

The bug is present in every release from 0.7.5 through HEAD.

Fix

Two call sites, same swap: replace performClick(None) with NSStatusItem.popUpStatusItemMenu(menu). That method positions the menu off the status item's own button frame and does not need an NSEvent to discover the active screen/Space.

popUpStatusItemMenu: was deprecated in macOS 10.10 in favour of binding the menu via statusItem.menu and letting AppKit handle the click. We cannot rely on that path here because the TrayTarget subview already intercepts mouseDown:, so the native button-click → menu route is short-circuited. The symbol is functionally intact on every supported macOS version (10.12+) and is the standard escape hatch when the click path is overridden.

Reentrancy: drop the RefCell::borrow() before the modal popup

popUpStatusItemMenu: is modal — it does not return until the menu is dismissed. A menu item action selected inside that modal can re-enter TrayIcon::set_menu, which calls menu.borrow_mut() on the same RefCell (src/platform_impl/macos/mod.rs:137). Holding a borrow() across the modal would panic with already borrowed: BorrowMutError for the very common case "user clicks an item whose handler rebuilds the menu".

on_tray_click clones the Retained<NSMenu> (an ObjC retain bump — single atomic increment, not a deep copy) and drops the RefCell borrow at the end of the same expression, so the inner NSMenu stays alive across the modal while the RefCell is free for set_menu to reborrow:

let menu = this.ivars().menu.borrow().as_ref().map(Retained::clone);
if let Some(menu) = menu {
    if menu.numberOfItems() > 0 {
        ns_button.highlight(true);
        this.ivars().status_item.popUpStatusItemMenu(&menu);
        ns_button.highlight(false);
        return;
    }
}

The show_menu() path reads self.attrs.menu (no RefCell involved) so it does not need the clone-and-drop dance.

Verification

  • cargo check clean on macOS; no new warnings introduced (the popUpStatusItemMenu deprecation is suppressed locally with #[allow(deprecated)] and a comment explaining why).
  • Verified end-to-end against a downstream app (Tauri 2.8.2 → tray-icon@0.21.3 with this patch applied through [patch.crates-io]). After the patch, the menu pops up reliably on a secondary display while the same display hosts a full-screen Space; previously it would silently no-op there. No regression on the other three (primary/secondary × desktop/full-screen) configurations.
  • Existing Click/Enter/Leave/Move event surface is unchanged; menu_on_left_click / menu_on_right_click semantics are preserved.

Notes for reviewers

  • I tried popUpMenuPositioningItem:atLocation:inView: first — it works too, but it needs the NSMenuItem feature flag added to objc2-app-kit and is slightly fussier about the view/location coordinate space. popUpStatusItemMenu: is the smallest possible change while being functionally equivalent for the status-bar use case.
  • Long-term, the cleaner fix would be to remove the TrayTarget subview's mouseDown: interception and forward to super.mouseDown(event) so AppKit's native click → popup path is restored. That is a much larger change and would alter the timing and ordering of the Click events surfaced to Rust callers, so I am keeping it out of scope here.

Closes #251.

…s#251

The previous implementation showed the menu by calling
`button.performClick(None)`, which synthesises a click with no NSEvent
context. AppKit's status-item popup logic relies on `[NSApp currentEvent]`
to discover the active screen/Space; when the synthesised click resolves
to the wrong context — specifically on a secondary display while a
full-screen Space is active there — the popup silently no-ops.

Drive the popup directly through `NSStatusItem.popUpStatusItemMenu`,
which positions the menu off the status item's own button frame. This
makes the menu appear reliably in every monitor/Space combination,
including secondary displays whose active Space hosts a full-screen
application.

`popUpStatusItemMenu:` was deprecated in macOS 10.10 (Apple recommends
binding the menu via `statusItem.menu` and letting AppKit handle the
click), but the symbol is functionally intact on every supported macOS
version and is the standard escape hatch when the custom subview already
intercepts mouse events.

The `on_tray_click` site also needs to release its `RefCell::borrow()`
before entering the modal popup: a menu item action selected inside the
modal can re-enter `TrayIcon::set_menu`, which calls `borrow_mut()` on
the same RefCell. We clone the `Retained<NSMenu>` (an ObjC retain bump)
and drop the borrow first so the modal cannot deadlock with itself.

Fixes tauri-apps#251.
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.

Menu not shown when icon clicked (macos)

2 participants