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) { HStack(spacing: 6) { Text("MENU BAR") .font(.caption2) .fontWeight(.semibold) .foregroundStyle(.secondary) Spacer() Text("\(menuBar.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 !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 { LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) { ForEach(menuBar.items) { item in MenuBarTile(item: item, menuBar: menuBar) } } .padding(.horizontal, 12) } 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 Wi-Fi, Spotlight, Clock, Battery and Control Center stay visible; macOS won't let any third-party app move them.") } } } 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) } } } }