macos: convert tab color view to SwiftUI

pull/9784/head
Mitchell Hashimoto 2025-12-11 13:18:25 -08:00
parent 04913905a3
commit a0089702f1
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 89 additions and 93 deletions

View File

@ -1,4 +1,5 @@
import AppKit
import SwiftUI
enum TerminalTabColor: Int, CaseIterable, Codable {
case none
@ -108,3 +109,74 @@ enum TerminalTabColor: Int, CaseIterable, Codable {
}
}
}
// MARK: - Menu View
/// A SwiftUI view displaying a color palette for tab color selection.
/// Used as a custom view inside an NSMenuItem in the tab context menu.
struct TabColorMenuView: View {
@State private var currentSelection: TerminalTabColor
let onSelect: (TerminalTabColor) -> Void
init(selectedColor: TerminalTabColor, onSelect: @escaping (TerminalTabColor) -> Void) {
self._currentSelection = State(initialValue: selectedColor)
self.onSelect = onSelect
}
var body: some View {
VStack(alignment: .leading, spacing: 3) {
ForEach(TerminalTabColor.paletteRows, id: \.self) { row in
HStack(spacing: 2) {
ForEach(row, id: \.self) { color in
TabColorSwatch(
color: color,
isSelected: color == currentSelection
) {
currentSelection = color
onSelect(color)
}
}
}
}
}
.padding(.leading, Self.leadingPadding)
.padding(.trailing, 12)
.padding(.top, 4)
.padding(.bottom, 4)
}
/// Leading padding to align with the menu's icon gutter.
/// macOS 26 introduced icons in menus, requiring additional padding.
private static var leadingPadding: CGFloat {
if #available(macOS 26.0, *) {
return 40
} else {
return 12
}
}
}
/// A single color swatch button in the tab color palette.
private struct TabColorSwatch: View {
let color: TerminalTabColor
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Group {
if color == .none {
Image(systemName: isSelected ? "circle.slash" : "circle")
.foregroundStyle(.secondary)
} else if let displayColor = color.displayColor {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle.fill")
.foregroundStyle(Color(nsColor: displayColor))
}
}
.font(.system(size: 16))
.frame(width: 20, height: 20)
}
.buttonStyle(.plain)
.help(color.localizedName)
}
}

View File

@ -349,7 +349,7 @@ class TerminalWindow: NSWindow {
}
removeTabColorSection(from: menu)
insertTabColorSection(into: menu, startingAt: Int(insertionIndex) + 1)
appendTabColorSection(to: menu)
}
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
@ -383,33 +383,29 @@ class TerminalWindow: NSWindow {
}
}
private func insertTabColorSection(into menu: NSMenu, startingAt index: Int) {
private func appendTabColorSection(to menu: NSMenu) {
guard let terminalController else { return }
var insertionIndex = index
let separator = NSMenuItem.separator()
separator.identifier = Self.tabColorSeparatorIdentifier
menu.insertItem(separator, at: insertionIndex)
insertionIndex += 1
menu.addItem(separator)
let headerTitle = NSLocalizedString("Tab Color", comment: "Tab color context menu section title")
let headerItem = NSMenuItem()
headerItem.identifier = Self.tabColorHeaderIdentifier
headerItem.title = headerTitle
headerItem.isEnabled = false
menu.insertItem(headerItem, at: insertionIndex)
insertionIndex += 1
headerItem.setImageIfDesired(systemSymbolName: "eyedropper")
menu.addItem(headerItem)
let paletteItem = NSMenuItem()
paletteItem.identifier = Self.tabColorPaletteIdentifier
let paletteView = TabColorPaletteView(
paletteItem.view = makeTabColorPaletteView(
selectedColor: tabColorSelection
) { [weak terminalController] color in
terminalController?.setTabColor(color)
}
paletteItem.view = paletteView
menu.insertItem(paletteItem, at: insertionIndex)
menu.addItem(paletteItem)
}
// MARK: Tab Key Equivalents
@ -781,86 +777,14 @@ private final class TabColorIndicator: NSView {
}
}
private final class TabColorPaletteView: NSView {
private let stackView = NSStackView()
private var selectedColor: TerminalTabColor
private let selectionHandler: (TerminalTabColor) -> Void
private var buttons: [NSButton] = []
init(selectedColor: TerminalTabColor,
selectionHandler: @escaping (TerminalTabColor) -> Void) {
self.selectedColor = selectedColor
self.selectionHandler = selectionHandler
super.init(frame: NSRect(origin: .zero, size: NSSize(width: 180, height: 60)))
stackView.orientation = .vertical
stackView.spacing = 6
addSubview(stackView)
for row in TerminalTabColor.paletteRows {
let rowStack = NSStackView()
rowStack.orientation = .horizontal
rowStack.spacing = 6
for color in row {
let button = makeButton(for: color)
rowStack.addArrangedSubview(button)
buttons.append(button)
}
stackView.addArrangedSubview(rowStack)
}
translatesAutoresizingMaskIntoConstraints = true
setFrameSize(intrinsicContentSize)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: NSSize {
NSSize(width: 190, height: 70)
}
override func layout() {
super.layout()
stackView.frame = bounds.insetBy(dx: 10, dy: 6)
}
private func makeButton(for color: TerminalTabColor) -> NSButton {
let button = NSButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.imagePosition = .imageOnly
button.imageScaling = .scaleProportionallyUpOrDown
button.image = color.swatchImage(selected: color == selectedColor)
button.setButtonType(.momentaryChange)
button.isBordered = false
button.focusRingType = .none
button.target = self
button.action = #selector(onSelectColor(_:))
button.tag = color.rawValue
button.toolTip = color.localizedName
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 24),
button.heightAnchor.constraint(equalToConstant: 24)
])
return button
}
@objc private func onSelectColor(_ sender: NSButton) {
guard let color = TerminalTabColor(rawValue: sender.tag) else { return }
selectedColor = color
updateButtonImages()
selectionHandler(color)
}
private func updateButtonImages() {
for button in buttons {
guard let color = TerminalTabColor(rawValue: button.tag) else { continue }
button.image = color.swatchImage(selected: color == selectedColor)
}
}
private func makeTabColorPaletteView(
selectedColor: TerminalTabColor,
selectionHandler: @escaping (TerminalTabColor) -> Void
) -> NSView {
let hostingView = NSHostingView(rootView: TabColorMenuView(
selectedColor: selectedColor,
onSelect: selectionHandler
))
hostingView.frame.size = hostingView.intrinsicContentSize
return hostingView
}