Hide menu bar icons with a cover instead of moving them off screen

Sliding the icons off the screen meant bringing them all back just to
open one menu, which flickered. Draw a frosted window over the other
apps' icons instead: they keep their real positions, so tapping a tile
opens its menu in place with no reveal, and the cover leaves a gap so
the hamburger is never hidden.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
This commit is contained in:
2026-09-27 03:06:15 +08:00
parent 2312e09105
commit 3e57efe312
6 changed files with 246 additions and 143 deletions
+26 -32
View File
@@ -120,38 +120,32 @@ no control over this, so the app creates its own `NSStatusItem` and:
`/tmp/simpletopbar-items.txt`; use it to verify the icon is not under the notch
(x should be ~1200, not ~765).
## Hiding other menu bar items
## Hiding other menu bar items (cover)
`MenuBarHider` creates a zero-width spacer `NSStatusItem` at launch and grows its
length to `hiddenLength` (10_000) to push the icons on its left off-screen. It is
**on by default** (`hideMenuBarItems` defaults to true, then persisted). Its
preferred position controls where the hidden/kept boundary lands:
`MenuBarHider` drives `MenuBarCover`, which hides other apps' icons by drawing
our own frosted window ("cover") over them instead of moving them off-screen.
Because the icons keep their real positions, `press` opens their menus in place
with no reveal. Verified with `--diag-cover` (cover windows at level 26) and
`--diag-press` (the WireGuard menu opens at a positive x, not off-screen).
- Seed **250** (default) puts the boundary just left of Wi-Fi, so every
third-party app icon goes to x < 0 (cmux, Kapture, moomoo, WireGuard,
TextInputMenuAgent, Now Playing) while **Wi-Fi (x≈1173), Spotlight, Clock,
Battery, Control Center and our hamburger stay visible**.
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
- Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits
off-screen, and its menu opens off-screen too (verified with `--diag-press`:
the menu window lands at negative x). So press briefly `suspend()`s the hider,
waits ~0.2s for the layout to settle, performs the accessibility action
(`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
menu is its own window anchored at the item's on-screen spot, so it survives
re-hiding.
- **Cannot reveal a single icon.** The spacer hides a contiguous block to its
left, so a press can only reveal the whole block. Isolating one item would
need either per-item ordering control (not available) or covering the icons
with an overlay window instead of moving them off-screen.
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
hamburger is created into the collapsed space and lands off-screen.
- **Wi-Fi/Spotlight/Clock/Battery/Control Center cannot be hidden**: macOS pins
them to the far right and a third-party item can never be inserted to their
right, so the spacer has nothing to push them with. This is a macOS
constraint, not a bug.
- `MenuBarCover.shouldHide` covers everything except the Apple keep-list
(Wi-Fi, Battery, Clock, Control Center, Spotlight/Search).
- `runs(from:)` merges adjacent item frames into contiguous cover windows;
`subtracting(_:from:)` carves out the hamburger so it is never covered
(`MenuBarItemManager.refresh` passes `avoid:` = our own item's frame).
- Frames come from AX (`kAXPositionAttribute` + `kAXSizeAttribute`) with a
**top-left origin**; `windowFrame(for:primaryHeight:)` flips Y for NSWindow.
- The cover window level is 26 (just above status items at 25, below menus at
~101), so opened menus draw on top of it.
- `ignoresMouseEvents = false` makes the covered strip swallow clicks so the
hidden icons cannot be clicked through.
### Legacy: off-screen spacer
An earlier approach used a zero-width spacer `NSStatusItem` grown to width
10_000 to push icons off the left edge. It is no longer used because it required
a visible reveal to open a menu. Findings if it is ever revived: preferred
position ~250 kept Wi-Fi while hiding apps, and `debug.spacerPos` overrode it.
## Gotchas
@@ -162,7 +156,7 @@ preferred position controls where the hidden/kept boundary lands:
- Reading `activationPolicy` is reliable but must happen on the main thread;
keep all `NSWorkspace` access inside the model.
- The menu bar tray needs Accessibility permission; do not add Screen Recording
or Apple Events usage. `MenuBarHider` must always be restored on quit, but
because a status item dies with its process, a crash cleans up on its own.
or Apple Events usage. `MenuBarHider` is restored on quit, and because the
cover windows belong to the process, a crash removes them automatically.
- `.gitignore` covers `.build/`, `SimpleTopBar.app/`, `dist/` — build artifacts
should never be committed.