Skip to content

fix(contextmenu): dismiss the flyout when the owning window loses focus - #572

Merged
kdroidFilter merged 1 commit into
nucleus-2.5from
fix/contextmenu-dismiss-on-focus-loss
Aug 18, 2026
Merged

fix(contextmenu): dismiss the flyout when the owning window loses focus#572
kdroidFilter merged 1 commit into
nucleus-2.5from
fix/contextmenu-dismiss-on-focus-loss

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Problem

Right-click to open the OS-looking context menu, then click into another application — the menu stays on screen. It should dismiss, like every native menu does.

Why

Each backend detects outside clicks by watching its own process only:

Backend Outside-click source Click in another app
macOS real NSMenu ✅ dismisses itself
Windows thread-local WH_MOUSE hook on the popup HWND ❌ never notified
Linux parent window's own pointer input (TaoPopupHostLinux.registerOutsidePressListener) ❌ never notified

The Windows hook is thread-local deliberately — it replaced a SetCapture monitor that forced the popup foreground, made the parent receive WM_KILLFOCUS and flipped WindowInfo.isWindowFocused false (see the comment in nucleus_tao_windows_popup.c).

On Linux the flyout is a scene layer, and a press that never lands on our surface is never reported — X11 and Wayland alike. Wayland forbids watching another surface's clicks by design. The global XI2 root-window monitor people may think of belongs to the standalone popup host (tray popups), not to this flyout.

On top of that, no backend dismissed on a focus change with no click behind it: Alt+Tab, the taskbar, a notification stealing activation.

Fix

Window focus is the one signal every backend does deliver (WindowEvent::Focused from tao — GDK focus-out on X11, wl_keyboard.leave on Wayland). Dismiss the flyout on the focused → unfocused transition.

Two details:

  • LocalWindowInfo is read from the parent scene. Inside Popup the layer publishes its own WindowInfo with isWindowFocused pinned to true.
  • A leading unfocused run is dropped, so a backend that has not reported focus yet when the menu opens cannot dismiss it immediately — the flyout counterpart of the not-yet-active accessory app fixed for NSMenu in a62349e.

No native code touched. macOS is unaffected.

Test

ContextMenuFocusDismissTest covers the transition logic: dismiss on focus loss, no dismiss while focused, no dismiss on a leading unfocused run, re-dismiss after a regain.

:nucleus-application:test, ktlintCheck and detekt pass.

Not verified on screen — reproducing needs a manual click into another app, which does not automate. A Wayland session in particular is reasoning-from-code, not observed. Residual gap either way: clicking a surface that takes no keyboard focus (some layer-shell docks, the desktop background on some compositors) produces no focus-out and leaves the menu open — not a regression.

Right-clicking to open the OS-looking context menu and then clicking into
another application left the menu on screen.

Each backend detects outside clicks by watching its own process only. On
Windows the popup HWND uses a thread-local WH_MOUSE hook (deliberately, to
replace a SetCapture monitor that made the parent receive WM_KILLFOCUS and
flipped WindowInfo.isWindowFocused false). On Linux the scene layer is told
about outside presses by the parent window's own pointer input, so a press
that never lands on our surface is never reported — on X11 and Wayland
alike, and Wayland forbids watching another surface's clicks by design.
The global XI2 root monitor belongs to the standalone popup host (tray
popups), not to this flyout. No backend dismissed on a focus change with
no click behind it either (Alt+Tab, the taskbar, a notification stealing
activation).

Window focus is the one signal every backend delivers, so dismiss on the
focused -> unfocused transition. LocalWindowInfo is read from the parent
scene: inside Popup the layer publishes its own WindowInfo with
isWindowFocused pinned to true. A leading unfocused run is dropped so a
backend that has not reported focus yet when the menu opens cannot dismiss
it immediately -- the flyout counterpart of the not-yet-active accessory
app fixed for NSMenu in a62349e.

macOS is unaffected: that path is a real NSMenu and already dismisses
itself.
@kdroidFilter
kdroidFilter merged commit 1c9fab2 into nucleus-2.5 Aug 18, 2026
23 checks passed
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