Skip to content

fix(v2): show the pill and menu bar panel over full screen apps - #142

Open
gauravlonari wants to merge 3 commits into
karansinghgit:mainfrom
gauravlonari:fix/macos-pill-over-fullscreen
Open

gauravlonari wants to merge 3 commits into
karansinghgit:mainfrom
gauravlonari:fix/macos-pill-over-fullscreen

Conversation

@gauravlonari

Copy link
Copy Markdown

What and why

On macOS the recorder pill and the menu bar panel only appeared on an empty desktop. As soon as any app was full screen, neither showed, so dictation gave no visual feedback at all in the case where you most want it — transcription and pasting worked, you just couldn't see anything.

Three things are needed for a window to join another app's full screen Space. Tauri exposes none of them, and the window stays invisible if any one is missing:

Needed What Tauri gives
NSWindowCollectionBehaviorFullScreenAuxiliary visibleOnAllWorkspaces only sets canJoinAllSpaces, which covers regular Spaces
NSStatusWindowLevel alwaysOnTop only reaches NSFloatingWindowLevel, too low to be admitted
being an NSPanel a plain NSWindow of an inactive app is kept out whatever its flags say

I confirmed each one is necessary rather than assuming: with the first two applied, isOnActiveSpace was still false over a full screen app, and only became true once the windows were panels.

SpeakType 1's recorder was already a nonactivating NSPanel with [.canJoinAllSpaces, .fullScreenAuxiliary] (speaktype/Controllers/MiniRecorderWindowController.swift), so this brings SpeakType 2 back in line with it.

Two follow-on changes come from the panel conversion:

  • Opening the panel no longer calls set_focus. tao implements it as makeKeyAndOrderFront: followed by activateIgnoringOtherApps:, and that activation switched the user to whichever Space the main window was on and dismissed the full screen app's menu bar. show() already sends makeKeyAndOrderFront:, and a nonactivating panel takes focus without activating the app.
  • The panel gets an NSPanel subclass restoring canBecomeKeyWindow. tao overrides that for borderless windows, and the override goes with the class. Without it the panel never becomes key, so it never resigns key, so it never closed on click-away.

Tested on

macOS 26.6.2, Apple Silicon. Not tested on Windows or Linux: both get no-op stubs, so behaviour there is unchanged by construction, but a second pair of eyes on that would be welcome.

  • Pill appears over a full screen app, and on the desktop
  • Menu bar panel opens over a full screen app, without switching Space or hiding the menu bar
  • Panel closes when clicking away
  • Pill positions, dictation, transcription and pasting all still work
  • Windows, Linux

Note for review

float_over_fullscreen changes the window's class in place with object_setClass. It is the same approach tauri-nspanel takes, and NSPanel declares no instance variables of its own, so it is sound — but it is undocumented, and it silently dropped tao's canBecomeKeyWindow override, which is what the subclass above restores. If a future tao version puts more behaviour on its window class, this would need revisiting. I'm happy to switch to tauri-nspanel instead if you'd rather have the maintained version, at the cost of a new dependency.

The second commit is unrelated housekeeping and can be dropped if you prefer: the modifier-hotkey event tap retried every two seconds and logged nothing when it failed, so a missing Accessibility permission was indistinguishable from a hotkey that did nothing. It now reports the first failure and says when it recovers.

🤖 Generated with Claude Code

@karansinghgit

Copy link
Copy Markdown
Owner

This is the right fix and it brings SpeakType 2 back in line with what SpeakType 1 did. The platform layer is used properly, all three systems get the new functions, every unsafe block carries a SAFETY note, and swapping panel.set_focus() for platform::focus_panel is what stops the Space switch.

One thing to fix before merge. Swapping the class to plain NSPanel discards tao's canBecomeKeyWindow and canBecomeMainWindow overrides, which are what make "focusable": false work for the pill. NSPanel answers YES to both, and with the nonactivating style bit the pill can take key status even while SpeakType is in the background, so it would start swallowing keystrokes meant for the app someone is dictating into. Clicking the pill mid-recording is a normal path, since it takes clicks then and has its own controls.

The fix is a small extension of what you already built: give the non-focusable case its own subclass whose canBecomeKeyWindow and canBecomeMainWindow return NO, rather than falling through to class!(NSPanel).

Two smaller notes:

  • tao::Window::set_focusable writes an ivar by name that plain NSPanel doesn't have, so it would abort the process on these two windows. Nothing calls it today, so it's latent, but it belongs in your review notes next to the canBecomeKeyWindow caveat.
  • The class swap also drops tao's sendEvent: override, which implements drag-by-background. Harmless here, since only the main window has drag regions, but the doc comment explains the risk in terms of instance variables when the real risk is losing tao's behaviour.

The Windows and Linux stubs are the right call. I'd soften the comment to say nothing extra is needed today, rather than that those systems never hide floating windows, which isn't reliably true for exclusive-fullscreen games or some Wayland compositors.

Your second commit, the hotkey logging, is good and independent of all this. Happy to take it either way.

gauravlonari and others added 3 commits September 23, 2026 18:00
On macOS both windows only appeared on an empty desktop. Three things are
needed for a window to join another app's full screen Space, and Tauri
gives none of them:

- `visibleOnAllWorkspaces` only sets `canJoinAllSpaces`, which covers
  regular Spaces, so `fullScreenAuxiliary` is set here.
- `alwaysOnTop` only reaches `NSFloatingWindowLevel`, too low to be
  admitted, so the level is raised to `NSStatusWindowLevel`.
- a plain `NSWindow` of an inactive app is kept out whatever its flags
  say, so both windows become nonactivating `NSPanel`s, as SpeakType 1's
  recorder already was.

Opening the panel no longer calls `set_focus`: it activates the app,
which switched the user to whichever Space the main window was on and
dismissed the full screen app's menu bar. `show()` already sends
`makeKeyAndOrderFront:`, and a nonactivating panel takes focus without
activating.

The panel also needs `canBecomeKeyWindow`, which tao overrides for
borderless windows and which is lost with its class, so it becomes an
`NSPanel` subclass that restores it. Without that the panel never
becomes key, never resigns key, and so never closes on click-away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The event tap retries every two seconds and said nothing when it failed,
so a missing Accessibility permission looked like a hotkey that simply
did nothing. Report the first failure, and say when it starts working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Swapping the class left `"focusable": false` meaning nothing: tao answers
canBecomeKeyWindow and canBecomeMainWindow from an ivar on the class the
window no longer has. Give each kind its own NSPanel subclass that
answers for itself, so the pill can't take key status from the app
someone is dictating into.

Say in the doc comment what changing the class actually costs: tao's two
focus overrides, its sendEvent: override for drag-by-background, and
set_focusable, which would now abort. Soften the Windows and Linux stubs
to say nothing extra is needed today, rather than claiming those systems
never cover floating windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gauravlonari
gauravlonari force-pushed the fix/macos-pill-over-fullscreen branch from f99d319 to eb6fc68 Compare September 23, 2026 12:31
@gauravlonari

Copy link
Copy Markdown
Author

@karansinghgit Done, and rebased on main so the conflict with #147 is gone.

The non-focusable case now has its own subclass. panel_class(focusable) registers two: SpeakTypeKeyPanel (canBecomeKeyWindow YES) for the menu bar panel, and SpeakTypeQuietPanel (NO) for the pill. Both answer NO to canBecomeMainWindow. Nothing falls through to plain NSPanel any more except if class registration itself fails.

Verified at runtime rather than assumed:

[probe] pill (focusable false): class "SpeakTypeQuietPanel", key false, main false
[probe] tray (focusable true):  class "SpeakTypeKeyPanel",  key true,  main false

One correction on the mechanism, since it may matter elsewhere. I probed the previous build before changing it, and a borderless nonactivating NSPanel answers NO to both on macOS 26.6.2:

[probe] pill (focusable false): canBecomeKeyWindow false, canBecomeMainWindow false

So the pill wasn't taking key status in practice, and I couldn't reproduce swallowed keystrokes while clicking it mid-recording. The change is still right, for the reason underneath your point: that NO was an undocumented AppKit default, not "focusable": false doing anything, since the ivar it reads went with the class. Now it's stated rather than inherited. If you've seen the YES behaviour on an older macOS, that's worth knowing — it would mean the pill was genuinely swallowing keys there.

Both smaller notes are in the doc comment, which now describes what changing the class costs in terms of tao's behaviour rather than instance variables: the two focus overrides, the sendEvent: override for drag-by-background (only matters for windows with drag regions, which these two don't have), and set_focusable, which writes the focusable ivar by name and would now abort, so nothing may call it on these windows.

Windows and Linux stubs softened to say nothing extra is needed today, noting exclusive full screen apps and some Wayland compositors as where that would change.

Checks are green on the rebase: cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test, npm run typecheck, npm test. Re-tested by hand on macOS 26.6.2 (Apple Silicon): pill over a full screen app, panel opening without a Space switch or losing the menu bar, panel closing on click-away, and dictation pasting into the app in front.

Happy to squash the three commits, or drop the hotkey logging one, whichever you prefer.

🤖 Generated with Claude Code

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.

2 participants