78 lines
2.5 KiB
Swift
78 lines
2.5 KiB
Swift
import AppKit
|
|||
|
|
import SwiftUI
|
||
|
|
|
||
|
|
/// What the tab bar shows: one tab per connection, in Settings' order.
|
||
|
|
final class TabBarModel: ObservableObject {
|
||
|
|
|
||
|
|
struct Tab: Identifiable, Equatable {
|
||
|
|
let id: UUID
|
||
|
|
var title: String
|
||
|
|
var address: String
|
||
|
|
var unreachable: Bool
|
||
|
|
}
|
||
|
|
|
||
|
|
@Published var tabs: [Tab] = []
|
||
|
|
@Published var selection: UUID?
|
||
|
|
var onSelect: (UUID) -> Void = { _ in }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Hosts the tabs. A click on a tab works even while another app is in
|
||
|
|
/// front, rather than only bringing the viewer forward.
|
||
|
|
final class TabBarHostingView: NSHostingView<TabBar> {
|
||
|
|
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The slim bar of installation names. It sits in the window's title bar
|
||
|
|
/// area, so it takes the title bar's material in light and dark alike.
|
||
|
|
struct TabBar: View {
|
||
|
|
|
||
|
|
@ObservedObject var model: TabBarModel
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
HStack(spacing: 4) {
|
||
|
|
ForEach(model.tabs) { tab in
|
||
|
|
TabButton(tab: tab, selected: tab.id == model.selection) { model.onSelect(tab.id) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.padding(.horizontal, 8)
|
||
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private struct TabButton: View {
|
||
|
|
|
||
|
|
let tab: TabBarModel.Tab
|
||
|
|
let selected: Bool
|
||
|
|
let action: () -> Void
|
||
|
|
@State private var hovering = false
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
Button(action: action) {
|
||
|
|
HStack(spacing: 5) {
|
||
|
|
if tab.unreachable {
|
||
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
||
|
|
.imageScale(.small)
|
||
|
|
.foregroundStyle(.orange)
|
||
|
|
}
|
||
|
|
Text(tab.title)
|
||
|
|
.lineLimit(1)
|
||
|
|
.truncationMode(.tail)
|
||
|
|
}
|
||
|
|
.font(.system(size: 12, weight: selected ? .semibold : .regular))
|
||
|
|
.foregroundStyle(selected ? .primary : .secondary)
|
||
|
|
.padding(.horizontal, 10)
|
||
|
|
.frame(maxWidth: .infinity, minHeight: 24)
|
||
|
|
.background(
|
||
|
|
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||
|
|
.fill(Color.primary.opacity(selected ? 0.12 : hovering ? 0.06 : 0))
|
||
|
|
)
|
||
|
|
.contentShape(Rectangle())
|
||
|
|
}
|
||
|
|
.buttonStyle(.plain)
|
||
|
|
.focusable(false) // the keyboard belongs to the dashboard; ⌃Tab switches
|
||
|
|
.onHover { hovering = $0 }
|
||
|
|
.help(tab.unreachable ? "\(tab.address) — not answering" : tab.address)
|
||
|
|
.accessibilityAddTraits(selected ? .isSelected : [])
|
||
|
|
}
|
||
|
|
}
|