Sources/OrgEditorAppKit/Theme.swift
91 lines · 4287 bytes
1#if os(macOS)
2import AppKit
3import OrgPresentation
4
5/// Attributes for each style role. A fixed theme for now; the app's theme settings replace it.
6@MainActor
7struct Theme {
8 let font = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
9
10 var base: [NSAttributedString.Key: Any] {
11 [.font: font, .foregroundColor: NSColor.textColor]
12 }
13
14 /// Width of one column of the monospaced font.
15 var column: CGFloat {
16 ("0" as NSString).size(withAttributes: [.font: font]).width
17 }
18
19 func paragraphStyle(firstLine: Int, wrapped: Int) -> NSParagraphStyle {
20 let style = NSMutableParagraphStyle()
21 style.firstLineHeadIndent = CGFloat(firstLine) * column
22 style.headIndent = CGFloat(wrapped) * column
23 return style
24 }
25
26 func apply(_ role: StyleRole, to storage: NSTextStorage, range: NSRange) {
27 switch role {
28 case .heading(let level):
29 let size = font.pointSize + CGFloat(max(0, 4 - level))
30 storage.addAttribute(.font, value: NSFont.monospacedSystemFont(ofSize: size, weight: .semibold), range: range)
31 case .leadingStars:
32 storage.addAttribute(.foregroundColor, value: NSColor.clear, range: range)
33 case .todo:
34 storage.addAttributes([.foregroundColor: NSColor.systemRed, .font: bold(storage, range)], range: range)
35 case .done:
36 storage.addAttributes([.foregroundColor: NSColor.systemGreen, .font: bold(storage, range)], range: range)
37 case .priority:
38 storage.addAttribute(.foregroundColor, value: NSColor.systemOrange, range: range)
39 case .tags, .comment, .keyword, .metadata:
40 storage.addAttribute(.foregroundColor, value: NSColor.secondaryLabelColor, range: range)
41 case .bold:
42 storage.addAttribute(.font, value: bold(storage, range), range: range)
43 case .italic:
44 storage.addAttribute(.font, value: NSFontManager.shared.convert(currentFont(storage, range), toHaveTrait: .italicFontMask), range: range)
45 case .underline:
46 storage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
47 case .strikeThrough:
48 storage.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: range)
49 case .verbatim, .code, .inlineSource:
50 storage.addAttribute(.backgroundColor, value: NSColor.quaternarySystemFill, range: range)
51 case .markup, .linkMarkup, .blockDelimiter:
52 storage.addAttribute(.foregroundColor, value: NSColor.tertiaryLabelColor, range: range)
53 case .link:
54 storage.addAttributes([.foregroundColor: NSColor.linkColor, .underlineStyle: NSUnderlineStyle.single.rawValue], range: range)
55 case .timestamp:
56 storage.addAttribute(.foregroundColor, value: NSColor.systemPurple, range: range)
57 case .footnote, .cookie, .target, .macro, .latex:
58 storage.addAttribute(.foregroundColor, value: NSColor.systemTeal, range: range)
59 case .block:
60 storage.addAttribute(.backgroundColor, value: NSColor.quaternarySystemFill, range: range)
61 case .table, .rule:
62 break
63 case .syntax(let category):
64 if let color = Self.color(category) { storage.addAttribute(.foregroundColor, value: color, range: range) }
65 }
66 }
67
68 /// Code colors, close to Xcode's default theme.
69 static func color(_ category: SyntaxCategory) -> NSColor? {
70 switch category {
71 case .keyword: .systemPink
72 case .string: .systemRed
73 case .comment: .secondaryLabelColor
74 case .function: .systemBlue
75 case .type: .systemTeal
76 case .number, .constant, .escape: .systemOrange
77 case .property, .attribute, .tag: .systemIndigo
78 case .label: .systemPurple
79 case .variable, .operator, .punctuation: nil
80 }
81 }
82
83 private func currentFont(_ storage: NSTextStorage, _ range: NSRange) -> NSFont {
84 storage.attribute(.font, at: range.location, effectiveRange: nil) as? NSFont ?? font
85 }
86
87 private func bold(_ storage: NSTextStorage, _ range: NSRange) -> NSFont {
88 NSFontManager.shared.convert(currentFont(storage, range), toHaveTrait: .boldFontMask)
89 }
90}
91#endif