Files
conjure/Sources/Conjure/ShortcutIndicator.swift
T

62 lines
2.5 KiB
Swift

import AppKit
import CoreText
@MainActor
final class ShortcutIndicator: NSStackView {
init(_ keys: String..., backgroundColor: NSColor? = nil) {
super.init(frame: .zero)
orientation = .horizontal
alignment = .centerY
spacing = 2
setHuggingPriority(.required, for: .horizontal)
setHuggingPriority(.required, for: .vertical)
translatesAutoresizingMaskIntoConstraints = false
setAccessibilityElement(false)
for key in keys {
let badge = ShortcutKeyBadge(labelWithString: key)
badge.font = .systemFont(ofSize: 12, weight: .medium)
badge.textColor = SunsetPalette.secondaryText
badge.alignment = .center
badge.wantsLayer = true
badge.layer?.cornerRadius = 5
badge.layer?.borderWidth = 1
badge.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
badge.layer?.backgroundColor = backgroundColor?.cgColor
badge.setAccessibilityElement(false)
badge.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
badge.widthAnchor.constraint(equalToConstant: 18),
badge.heightAnchor.constraint(equalToConstant: 18)
])
addArrangedSubview(badge)
}
}
func setSingleKey(_ key: String) {
precondition(arrangedSubviews.count == 1)
(arrangedSubviews[0] as? NSTextField)?.stringValue = key
}
required init?(coder: NSCoder) { fatalError("Conjure creates shortcut indicators programmatically") }
}
@MainActor
private final class ShortcutKeyBadge: NSTextField {
// NSTextField's horizontal alignment insets otherwise make an 18-point badge 22 points wide.
override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
let line = CTLineCreateWithAttributedString(attributedStringValue)
let ink = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
context.saveGState()
context.textMatrix = .identity
// Center visible ink, not the font's asymmetric ascender/descender line box.
context.translateBy(x: bounds.midX - ink.midX, y: bounds.midY + ink.midY)
context.scaleBy(x: 1, y: -1)
context.textPosition = .zero
CTLineDraw(line, context)
context.restoreGState()
}
}