krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgEditorAppKit/Keys.swift

abca8dc581329472f7fb8ccd3af147e5fb882f29
orgstar/Sources/OrgEditorAppKit/Keys.swift history · blame · raw

105 lines · 4213 bytes

  1#if os(macOS)
  2import AppKit
  3import OrgCore
  4
  5/// Which Option keys act as Meta. The others type characters, as usual on a Mac.
  6public struct OptionAsMeta: OptionSet, Sendable {
  7    public let rawValue: Int
  8    public init(rawValue: Int) { self.rawValue = rawValue }
  9    public static let left = OptionAsMeta(rawValue: 1)
 10    public static let right = OptionAsMeta(rawValue: 2)
 11}
 12
 13/// A text view that offers key presses to the editor's keymap before the text system.
 14final class OrgTextView: NSTextView {
 15    /// Returns true when the key was handled.
 16    var keyHandler: ((NSEvent) -> Bool)?
 17    var onMouseDown: (() -> Void)?
 18    /// Draw the caret as a block over the next character, for normal and visual states.
 19    var blockCursor = false {
 20        didSet {
 21            guard blockCursor != oldValue else { return }
 22            needsDisplay = true
 23        }
 24    }
 25
 26    override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
 27        guard blockCursor else { return super.drawInsertionPoint(in: rect, color: color, turnedOn: flag) }
 28        var block = rect
 29        block.size.width = max(rect.width, blockWidth)
 30        if flag {
 31            color.withAlphaComponent(0.45).setFill()
 32            block.fill(using: .sourceOver)
 33        } else {
 34            setNeedsDisplay(block, avoidAdditionalLayout: true)
 35        }
 36    }
 37
 38    override func setNeedsDisplay(_ rect: NSRect, avoidAdditionalLayout flag: Bool) {
 39        var widened = rect
 40        if blockCursor { widened.size.width += blockWidth }
 41        super.setNeedsDisplay(widened, avoidAdditionalLayout: flag)
 42    }
 43
 44    private var blockWidth: CGFloat {
 45        let font = self.font ?? NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
 46        return ("x" as NSString).size(withAttributes: [.font: font]).width
 47    }
 48
 49    override func keyDown(with event: NSEvent) {
 50        if !hasMarkedText(), keyHandler?(event) == true { return }
 51        super.keyDown(with: event)
 52    }
 53
 54    override func performKeyEquivalent(with event: NSEvent) -> Bool {
 55        if window?.firstResponder === self, !hasMarkedText(), event.modifierFlags.contains(.command), keyHandler?(event) == true {
 56            return true
 57        }
 58        return super.performKeyEquivalent(with: event)
 59    }
 60
 61    override func mouseDown(with event: NSEvent) {
 62        onMouseDown?()
 63        super.mouseDown(with: event)
 64    }
 65}
 66
 67enum KeyEvents {
 68    /// Device-dependent bits for the left and right Option keys (`NX_DEVICELALTKEYMASK`,
 69    /// `NX_DEVICERALTKEYMASK`).
 70    static let leftOption: UInt = 0x20
 71    static let rightOption: UInt = 0x40
 72
 73    static let named: [UInt16: String] = [
 74        48: "TAB", 36: "RET", 76: "RET", 49: "SPC", 53: "ESC", 51: "DEL", 117: "<delete>",
 75        123: "<left>", 124: "<right>", 125: "<down>", 126: "<up>",
 76        115: "<home>", 119: "<end>", 116: "<prior>", 121: "<next>",
 77        122: "<f1>", 120: "<f2>", 99: "<f3>", 118: "<f4>", 96: "<f5>", 97: "<f6>",
 78        98: "<f7>", 100: "<f8>", 101: "<f9>", 109: "<f10>", 103: "<f11>", 111: "<f12>",
 79    ]
 80
 81    /// The chord for a key press, or nil when it types a character with Option.
 82    static func chord(for event: NSEvent, optionAsMeta: OptionAsMeta) -> KeyChord? {
 83        guard event.type == .keyDown else { return nil }
 84        let flags = event.modifierFlags
 85        var modifiers: KeyChord.Modifiers = []
 86        if flags.contains(.control) { modifiers.insert(.control) }
 87        if flags.contains(.command) { modifiers.insert(.command) }
 88        if flags.contains(.option) {
 89            let raw = flags.rawValue
 90            let meta = (raw & leftOption != 0 && optionAsMeta.contains(.left)) || (raw & rightOption != 0 && optionAsMeta.contains(.right))
 91            if meta || !modifiers.isEmpty {
 92                modifiers.insert(.meta)
 93            } else {
 94                return nil
 95            }
 96        }
 97        if let name = named[event.keyCode] {
 98            if flags.contains(.shift) { modifiers.insert(.shift) }
 99            return KeyChord(name, modifiers)
100        }
101        guard let characters = event.charactersIgnoringModifiers, characters.count == 1 else { return nil }
102        return KeyChord(characters, modifiers)
103    }
104}
105#endif