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:
@@ -0,0 +1,124 @@
|
||||
import AppKit
|
||||
|
||||
/// Hides other apps' menu bar icons by drawing a frosted window ("cover") over
|
||||
/// them, instead of moving them off-screen. The icons keep their real positions,
|
||||
/// so their menus open in the right place with no reveal, and a target item's
|
||||
/// real icon can be left visible by cutting a hole in the cover.
|
||||
@MainActor
|
||||
final class MenuBarCover {
|
||||
static let shared = MenuBarCover()
|
||||
|
||||
/// Just above the status items (level 25) but well below menus (~101).
|
||||
private static let windowLevel = NSWindow.Level(rawValue: 26)
|
||||
|
||||
private var windows: [NSWindow] = []
|
||||
|
||||
private init() {}
|
||||
|
||||
func update(enabled: Bool, items: [MenuBarItem], avoid: CGRect? = nil) {
|
||||
removeAllWindows()
|
||||
guard enabled else { return }
|
||||
|
||||
let frames = items.compactMap { item -> CGRect? in
|
||||
guard Self.shouldHide(item), let frame = item.frame else { return nil }
|
||||
return frame
|
||||
}
|
||||
var runs = Self.runs(from: frames)
|
||||
if let avoid {
|
||||
runs = Self.subtracting(avoid, from: runs)
|
||||
}
|
||||
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
||||
for run in runs {
|
||||
let frame = Self.windowFrame(for: run, primaryHeight: primaryHeight)
|
||||
windows.append(makeWindow(frame: frame))
|
||||
}
|
||||
}
|
||||
|
||||
/// Which items the cover hides. Everything is covered except the Apple
|
||||
/// items the user asked to keep visible.
|
||||
nonisolated static func shouldHide(_ item: MenuBarItem) -> Bool {
|
||||
let text = item.title.lowercased()
|
||||
.replacingOccurrences(of: "\u{2010}", with: "-")
|
||||
.replacingOccurrences(of: "\u{2011}", with: "-")
|
||||
let keep = ["wi-fi", "wifi", "battery", "clock", "control center", "search", "spotlight"]
|
||||
return !keep.contains(where: { text.contains($0) })
|
||||
}
|
||||
|
||||
/// Merge overlapping or near frames (in Accessibility coordinates) into
|
||||
/// contiguous runs, so adjacent icons get one cover window rather than many.
|
||||
nonisolated static func runs(from frames: [CGRect], gap: CGFloat = 6) -> [CGRect] {
|
||||
let sorted = frames.sorted { $0.minX < $1.minX }
|
||||
var result: [CGRect] = []
|
||||
for frame in sorted {
|
||||
if let last = result.last, frame.minX <= last.maxX + gap {
|
||||
result[result.count - 1] = last.union(frame)
|
||||
} else {
|
||||
result.append(frame)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Carve `avoid` out of the runs so the cover never hides our own icon.
|
||||
nonisolated static func subtracting(_ avoid: CGRect, from runs: [CGRect]) -> [CGRect] {
|
||||
var result: [CGRect] = []
|
||||
for run in runs {
|
||||
guard run.intersects(avoid) else {
|
||||
result.append(run)
|
||||
continue
|
||||
}
|
||||
if avoid.minX > run.minX {
|
||||
result.append(CGRect(x: run.minX, y: run.minY, width: avoid.minX - run.minX, height: run.height))
|
||||
}
|
||||
if avoid.maxX < run.maxX {
|
||||
result.append(CGRect(x: avoid.maxX, y: run.minY, width: run.maxX - avoid.maxX, height: run.height))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Accessibility coordinates use a top-left origin; NSWindow uses a
|
||||
/// bottom-left one, so flip Y against the primary screen's height.
|
||||
nonisolated static func windowFrame(for axFrame: CGRect, primaryHeight: CGFloat) -> CGRect {
|
||||
CGRect(
|
||||
x: axFrame.minX,
|
||||
y: primaryHeight - axFrame.maxY,
|
||||
width: axFrame.width,
|
||||
height: axFrame.height
|
||||
)
|
||||
}
|
||||
|
||||
private func makeWindow(frame: CGRect) -> NSWindow {
|
||||
let window = NSWindow(
|
||||
contentRect: frame,
|
||||
styleMask: [.borderless],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.level = Self.windowLevel
|
||||
window.isOpaque = false
|
||||
window.backgroundColor = .clear
|
||||
window.hasShadow = false
|
||||
// Swallow clicks so the covered icons cannot be clicked through.
|
||||
window.ignoresMouseEvents = false
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary, .ignoresCycle]
|
||||
window.animationBehavior = .none
|
||||
|
||||
let effect = NSVisualEffectView(frame: CGRect(origin: .zero, size: frame.size))
|
||||
effect.material = .menu
|
||||
effect.blendingMode = .behindWindow
|
||||
effect.state = .active
|
||||
effect.autoresizingMask = [.width, .height]
|
||||
window.contentView = effect
|
||||
|
||||
window.orderFrontRegardless()
|
||||
return window
|
||||
}
|
||||
|
||||
private func removeAllWindows() {
|
||||
for window in windows {
|
||||
window.orderOut(nil)
|
||||
}
|
||||
windows.removeAll()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user