import AppKit @MainActor final class ActionPill: NSView { private var themeObservation: ThemeObservation? init(primaryButton: NSButton, actionsButton: NSButton, primaryCommand: String? = nil) { super.init(frame: .zero) wantsLayer = true layer?.cornerRadius = 18 layer?.borderWidth = 0.5 layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor for button in [primaryButton, actionsButton] { button.isBordered = false button.font = .systemFont(ofSize: 12, weight: .medium) button.contentTintColor = ThemePalette.text } primaryButton.keyEquivalent = primaryCommand ?? "\r" primaryButton.keyEquivalentModifierMask = primaryCommand == nil ? [] : .command actionsButton.title = "Actions" actionsButton.keyEquivalent = "k" actionsButton.keyEquivalentModifierMask = .command let returnKey = primaryCommand.map { ShortcutIndicator("⌘", $0.uppercased()) } ?? ShortcutIndicator("↵") let actionKeys = ShortcutIndicator("⌘", "K") for control in [primaryButton, returnKey, actionsButton, actionKeys] { control.translatesAutoresizingMaskIntoConstraints = false addSubview(control) } NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: 36), primaryButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), primaryButton.centerYAnchor.constraint(equalTo: centerYAnchor), returnKey.leadingAnchor.constraint(equalTo: primaryButton.trailingAnchor, constant: 8), returnKey.centerYAnchor.constraint(equalTo: centerYAnchor), actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14), actionsButton.centerYAnchor.constraint(equalTo: centerYAnchor), actionKeys.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8), actionKeys.centerYAnchor.constraint(equalTo: centerYAnchor), actionKeys.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), ]) themeObservation = ThemeObservation { [weak self, weak primaryButton, weak actionsButton] in self?.layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor self?.layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor primaryButton?.contentTintColor = ThemePalette.text actionsButton?.contentTintColor = ThemePalette.text } } required init?(coder: NSCoder) { fatalError("Conjure creates action pills programmatically") } } /// Keeps palette subscriptions scoped to the existing control's lifetime. @MainActor final class ThemeObservation { nonisolated(unsafe) private let token: NSObjectProtocol init(_ apply: @escaping @MainActor () -> Void) { ThemePalette.appearance.performAsCurrentDrawingAppearance(apply) token = NotificationCenter.default.addObserver( forName: ThemePalette.didChangeNotification, object: nil, queue: .main ) { _ in MainActor.assumeIsolated { ThemePalette.appearance.performAsCurrentDrawingAppearance(apply) } } } deinit { NotificationCenter.default.removeObserver(token) } } extension NSSearchField { /// Recolors the current presentation without changing the query or field-editor selection. func applyTheme() { textColor = ThemePalette.text if let placeholderAttributedString { let placeholder = NSMutableAttributedString(attributedString: placeholderAttributedString) placeholder.addAttribute(.foregroundColor, value: ThemePalette.secondaryText, range: NSRange(location: 0, length: placeholder.length)) self.placeholderAttributedString = placeholder } if let editor = currentEditor() as? NSTextView { editor.textColor = ThemePalette.text } } }