Files
simpletopbar/Sources/SimpleTopBar/Overlay.swift
T
TZGyn 3e57efe312 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
2026-09-27 03:06:15 +08:00

125 lines
4.6 KiB
Swift

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()
}
}