Files
TZGyn abb0b548bf Keep all system icons visible and click Spotlight properly
The spacer boundary cut off Now Playing and the input source, which
should stay like Wi-Fi, so move it left of every Apple item. Spotlight's
status item ignores the accessibility press action, so click it at its
real position instead.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:35:20 +08:00

329 lines
12 KiB
Swift

import SwiftUI
import AppKit
/// The top bar panel shown from the menu bar hamburger icon.
struct TopBarView: View {
@ObservedObject var model: AppListModel
@ObservedObject var menuBar: MenuBarItemManager
@ObservedObject var hider: MenuBarHider
var body: some View {
VStack(spacing: 0) {
header
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 0) {
menuBarSection
Divider().padding(.vertical, 4)
section(title: "Apps", apps: model.regularApps, emptyText: "No apps running")
Divider().padding(.vertical, 4)
backgroundSection
}
.padding(.vertical, 4)
}
.frame(maxHeight: 420)
Divider()
footer
}
.frame(width: 320)
.onAppear {
model.refresh()
menuBar.refresh()
}
}
private var header: some View {
HStack(spacing: 8) {
Image(systemName: "line.3.horizontal")
.foregroundStyle(.secondary)
Text("SimpleTopBar")
.font(.headline)
Spacer()
Button {
model.refresh()
menuBar.refresh()
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.plain)
.help("Refresh")
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
/// The hamburger tray: every other app's menu bar item lives here.
private var menuBarSection: some View {
VStack(alignment: .leading, spacing: 0) {
if !menuBar.authorized {
VStack(alignment: .leading, spacing: 4) {
Text("Grant Accessibility to collect and open menu bar items.")
.font(.caption)
.foregroundStyle(.secondary)
Button("Grant Accessibility…") {
AccessibilityPermission.request()
menuBar.refresh()
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else if menuBar.items.isEmpty {
Text("No menu bar items found")
.font(.caption)
.foregroundStyle(.tertiary)
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else {
trayGroup(title: "Apps", items: appTrayItems)
Divider().padding(.vertical, 2)
trayGroup(title: "System", items: systemTrayItems)
}
if menuBar.authorized {
Toggle(isOn: $hider.enabled) {
Text("Hide items from the menu bar")
.font(.caption)
}
.toggleStyle(.switch)
.controlSize(.mini)
.padding(.horizontal, 12)
.padding(.top, 6)
.help("Best-effort: pushes other apps' icons off the menu bar so they only live here. Apple's system items (Wi-Fi, Now Playing, Clock, Battery, Control Center, Spotlight) stay visible; macOS won't let any third-party app move them.")
}
}
}
/// Third-party apps' menu bar icons.
private var appTrayItems: [MenuBarItem] {
menuBar.items.filter { !$0.isSystem }
}
/// Apple's own menu bar items (Control Center, Spotlight, …).
private var systemTrayItems: [MenuBarItem] {
menuBar.items.filter { $0.isSystem }
}
private func trayGroup(title: String, items: [MenuBarItem]) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 6) {
Text(title.uppercased())
.font(.caption2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Spacer()
Text("\(items.count)")
.font(.caption2)
.foregroundStyle(.tertiary)
.padding(.horizontal, 6)
.padding(.vertical, 1)
.background(Capsule().fill(Color.secondary.opacity(0.15)))
}
.padding(.horizontal, 12)
.padding(.top, 4)
.padding(.bottom, 4)
if items.isEmpty {
Text("None")
.font(.caption)
.foregroundStyle(.tertiary)
.padding(.horizontal, 12)
.padding(.bottom, 4)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
ForEach(items) { item in
MenuBarTile(item: item, menuBar: menuBar)
}
}
.padding(.horizontal, 12)
}
}
}
private func section(title: String, apps: [RunningApp], emptyText: String) -> some View {
VStack(alignment: .leading, spacing: 0) {
Text(title.uppercased())
.font(.caption2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 12)
.padding(.top, 4)
.padding(.bottom, 2)
if apps.isEmpty {
Text(emptyText)
.font(.caption)
.foregroundStyle(.tertiary)
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else {
ForEach(apps) { app in
AppRow(app: app, model: model)
}
}
}
}
/// Collapsible dropdown for background apps. When collapsed it is a single
/// header row; when expanded it reveals a limited number of apps and hides
/// the overflow behind "Show N more".
private var backgroundSection: some View {
VStack(alignment: .leading, spacing: 0) {
Button {
model.toggleBackgroundExpanded()
} label: {
HStack(spacing: 6) {
Image(systemName: model.backgroundExpanded ? "chevron.down" : "chevron.right")
.font(.caption2)
.foregroundStyle(.secondary)
.frame(width: 10)
Text("Background Apps")
.font(.caption2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Spacer()
Text("\(model.backgroundApps.count)")
.font(.caption2)
.foregroundStyle(.tertiary)
.padding(.horizontal, 6)
.padding(.vertical, 1)
.background(Capsule().fill(Color.secondary.opacity(0.15)))
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.help(model.backgroundExpanded ? "Collapse background apps" : "Expand background apps")
if model.backgroundExpanded {
if model.backgroundApps.isEmpty {
Text("No background apps")
.font(.caption)
.foregroundStyle(.tertiary)
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else {
ForEach(model.visibleBackgroundApps) { app in
AppRow(app: app, model: model)
}
if model.canShowMoreBackgroundApps {
Button {
model.showMoreBackgroundApps()
} label: {
Label(
"Show \(model.hiddenBackgroundCount) more",
systemImage: "ellipsis.circle"
)
.font(.caption)
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 4)
}
}
}
}
}
private var footer: some View {
HStack(spacing: 8) {
Text("\(model.regularApps.count) apps · \(model.backgroundApps.count) background")
.font(.caption2)
.foregroundStyle(.tertiary)
Spacer()
Button(LoginItem.isEnabled ? "Disable Launch at Login" : "Launch at Login") {
if !LoginItem.toggle() {
let alert = NSAlert()
alert.messageText = "Could not update Launch at Login"
alert.informativeText = "Launch at Login requires a signed app bundle in /Applications. It is not available when running via `swift run`."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
.font(.caption2)
.buttonStyle(.plain)
Button("Quit") { NSApp.terminate(nil) }
.font(.caption2)
.buttonStyle(.plain)
.keyboardShortcut("q")
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
}
private struct MenuBarTile: View {
let item: MenuBarItem
@ObservedObject var menuBar: MenuBarItemManager
var body: some View {
Button {
menuBar.press(item)
} label: {
// Preserve the symbol's aspect ratio: forcing wide symbols like the
// battery into a square stretches them into a blocky shape.
Image(nsImage: item.icon)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(Color.secondary.opacity(0.12))
)
}
.buttonStyle(.plain)
.help("\(item.appName) — \(item.title)")
}
}
private struct AppRow: View {
let app: RunningApp
@ObservedObject var model: AppListModel
var body: some View {
Button {
model.activate(app)
} label: {
HStack(spacing: 8) {
Image(nsImage: app.icon)
.resizable()
.frame(width: 18, height: 18)
Text(app.name)
.font(.system(size: 12))
.lineLimit(1)
.truncationMode(.middle)
if app.isActive {
Image(systemName: "circle.fill")
.font(.system(size: 5))
.foregroundStyle(Color.accentColor)
}
Spacer(minLength: 0)
}
.contentShape(Rectangle())
.padding(.horizontal, 12)
.padding(.vertical, 3)
.opacity(app.isHidden ? 0.55 : 1)
}
.buttonStyle(.plain)
.help("Activate \(app.name)")
.contextMenu {
Button("Activate") { model.activate(app) }
Button(app.isHidden ? "Unhide" : "Hide") { model.toggleHidden(app) }
Divider()
Button("Quit \(app.name)") { model.quit(app) }
}
}
}