import SwiftUI import AppKit /// The top bar panel shown from the menu bar icon. struct TopBarView: View { @ObservedObject var model: AppListModel var body: some View { VStack(spacing: 0) { header Divider() ScrollView { VStack(alignment: .leading, spacing: 0) { section(title: "Apps", apps: model.regularApps, emptyText: "No apps running") Divider().padding(.vertical, 4) backgroundSection } .padding(.vertical, 4) } .frame(maxHeight: 360) Divider() footer } .frame(width: 320) } private var header: some View { HStack(spacing: 8) { Image(systemName: "menubar.rectangle") .foregroundStyle(.secondary) Text("SimpleTopBar") .font(.headline) Spacer() Button { model.refresh() } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.plain) .help("Refresh") } .padding(.horizontal, 12) .padding(.vertical, 8) } 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 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) } } } }