import AppKit import CoreText @MainActor final class ShortcutIndicator: NSStackView { init(_ keys: String..., backgroundColor: NSColor? = nil, fontSize: CGFloat = 12, keySize: CGFloat = 18) { 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: fontSize, weight: .medium) badge.textColor = ThemePalette.secondaryText badge.alignment = .center badge.wantsLayer = true badge.layer?.cornerRadius = 5 badge.layer?.borderWidth = 1 badge.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.35).cgColor badge.layer?.backgroundColor = backgroundColor?.cgColor badge.setAccessibilityElement(false) badge.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ badge.widthAnchor.constraint(equalToConstant: keySize), badge.heightAnchor.constraint(equalToConstant: keySize) ]) 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() } }