From b5b5a8f8d13605ae8b94e147965686382482d9d6 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:59:35 -0600 Subject: [PATCH 1/6] Add Bold and Italics Support To Themes --- .../Extensions/EditorTheme+Default.swift | 32 ++--- .../TextViewController+Highlighter.swift | 2 +- .../Controller/TextViewController.swift | 2 +- .../Theme/EditorTheme.swift | 123 +++++++++++------- 4 files changed, 92 insertions(+), 67 deletions(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Extensions/EditorTheme+Default.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Extensions/EditorTheme+Default.swift index fceebc8d3..676eece9a 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Extensions/EditorTheme+Default.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Extensions/EditorTheme+Default.swift @@ -12,22 +12,22 @@ import CodeEditSourceEditor extension EditorTheme { static var standard: EditorTheme { EditorTheme( - text: .init(hex: "000000"), - insertionPoint: .init(hex: "000000"), - invisibles: .init(hex: "D6D6D6"), - background: .init(hex: "FFFFFF"), - lineHighlight: .init(hex: "ECF5FF"), - selection: .init(hex: "B2D7FF"), - keywords: .init(hex: "9B2393"), - commands: .init(hex: "326D74"), - types: .init(hex: "0B4F79"), - attributes: .init(hex: "815F03"), - variables: .init(hex: "0F68A0"), - values: .init(hex: "6C36A9"), - numbers: .init(hex: "1C00CF"), - strings: .init(hex: "C41A16"), - characters: .init(hex: "1C00CF"), - comments: .init(hex: "267507") + text: Attribute(color: NSColor(hex: "000000")), + insertionPoint: NSColor(hex: "000000"), + invisibles: Attribute(color: NSColor(hex: "D6D6D6")), + background: NSColor(hex: "FFFFFF"), + lineHighlight: NSColor(hex: "ECF5FF"), + selection: NSColor(hex: "B2D7FF"), + keywords: Attribute(color: NSColor(hex: "9B2393"), bold: true), + commands: Attribute(color: NSColor(hex: "326D74")), + types: Attribute(color: NSColor(hex: "0B4F79")), + attributes: Attribute(color: NSColor(hex: "815F03")), + variables: Attribute(color: NSColor(hex: "0F68A0")), + values: Attribute(color: NSColor(hex: "6C36A9")), + numbers: Attribute(color: NSColor(hex: "1C00CF")), + strings: Attribute(color: NSColor(hex: "C41A16"), bold: true, italic: true), + characters: Attribute(color: NSColor(hex: "1C00CF")), + comments: Attribute(color: NSColor(hex: "267507"), italic: true) ) } } diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift index fddc03654..f9ea82368 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift @@ -29,7 +29,7 @@ extension TextViewController { extension TextViewController: ThemeAttributesProviding { public func attributesFor(_ capture: CaptureName?) -> [NSAttributedString.Key: Any] { [ - .font: font, + .font: theme.fontFor(for: capture, from: font), .foregroundColor: theme.colorFor(capture), .kern: textView.kern ] diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index cafbbc4b6..f9e69a537 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -262,7 +262,7 @@ public class TextViewController: NSViewController { self.textView = TextView( string: string, font: font, - textColor: theme.text, + textColor: theme.text.color, lineHeightMultiplier: lineHeightMultiple, wrapLines: wrapLines, isEditable: isEditable, diff --git a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift index 9118fb2b0..8fe8b9339 100644 --- a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift +++ b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift @@ -7,43 +7,67 @@ import SwiftUI -/// A collection of `NSColor` used for syntax higlighting -public struct EditorTheme { +/// A collection of attributes used for syntax highlighting and other colors for the editor. +/// +/// Attributes of a theme that do not apply to text (background, line highlight) are a single `NSColor` for simplicity. +/// All other attributes use the ``EditorTheme/Attribute`` type to store +public struct EditorTheme: Equatable { + /// Represents attributes that can be applied to style text. + public struct Attribute: Equatable, Hashable, Sendable { + let color: NSColor + let bold: Bool + let italic: Bool - public var text: NSColor + public init(color: NSColor, bold: Bool = false, italic: Bool = false) { + self.color = color + self.bold = bold + self.italic = italic + } + + var fontDescriptorTraits: NSFontDescriptor.SymbolicTraits { + switch (bold, italic) { + case (true, true): return [.bold, .italic] + case (true, false): return [.bold] + case (false, true): return [.italic] + case (false, false): return [] + } + } + } + + public var text: Attribute public var insertionPoint: NSColor - public var invisibles: NSColor + public var invisibles: Attribute public var background: NSColor public var lineHighlight: NSColor public var selection: NSColor - public var keywords: NSColor - public var commands: NSColor - public var types: NSColor - public var attributes: NSColor - public var variables: NSColor - public var values: NSColor - public var numbers: NSColor - public var strings: NSColor - public var characters: NSColor - public var comments: NSColor + public var keywords: Attribute + public var commands: Attribute + public var types: Attribute + public var attributes: Attribute + public var variables: Attribute + public var values: Attribute + public var numbers: Attribute + public var strings: Attribute + public var characters: Attribute + public var comments: Attribute public init( - text: NSColor, + text: Attribute, insertionPoint: NSColor, - invisibles: NSColor, + invisibles: Attribute, background: NSColor, lineHighlight: NSColor, selection: NSColor, - keywords: NSColor, - commands: NSColor, - types: NSColor, - attributes: NSColor, - variables: NSColor, - values: NSColor, - numbers: NSColor, - strings: NSColor, - characters: NSColor, - comments: NSColor + keywords: Attribute, + commands: Attribute, + types: Attribute, + attributes: Attribute, + variables: Attribute, + values: Attribute, + numbers: Attribute, + strings: Attribute, + characters: Attribute, + comments: Attribute ) { self.text = text self.insertionPoint = insertionPoint @@ -63,10 +87,10 @@ public struct EditorTheme { self.comments = comments } - /// Get the color from ``theme`` for the specified capture name. - /// - Parameter capture: The capture name - /// - Returns: A `NSColor` - func colorFor(_ capture: CaptureName?) -> NSColor { + /// Maps a capture type to the attributes for that capture determined by the theme. + /// - Parameter capture: The capture to map to. + /// - Returns: Theme attributes for the capture. + private func mapCapture(_ capture: CaptureName?) -> Attribute { switch capture { case .include, .constructor, .keyword, .boolean, .variableBuiltin, .keywordReturn, .keywordFunction, .repeat, .conditional, .tag: @@ -82,25 +106,26 @@ public struct EditorTheme { default: return text } } -} -extension EditorTheme: Equatable { - public static func == (lhs: EditorTheme, rhs: EditorTheme) -> Bool { - return lhs.text == rhs.text && - lhs.insertionPoint == rhs.insertionPoint && - lhs.invisibles == rhs.invisibles && - lhs.background == rhs.background && - lhs.lineHighlight == rhs.lineHighlight && - lhs.selection == rhs.selection && - lhs.keywords == rhs.keywords && - lhs.commands == rhs.commands && - lhs.types == rhs.types && - lhs.attributes == rhs.attributes && - lhs.variables == rhs.variables && - lhs.values == rhs.values && - lhs.numbers == rhs.numbers && - lhs.strings == rhs.strings && - lhs.characters == rhs.characters && - lhs.comments == rhs.comments + /// Get the color from ``theme`` for the specified capture name. + /// - Parameter capture: The capture name + /// - Returns: A `NSColor` + func colorFor(_ capture: CaptureName?) -> NSColor { + return mapCapture(capture).color + } + + /// Returns the correct font with attributes (bold and italics) for a given capture name. + /// - Parameters: + /// - capture: The capture name. + /// - font: The font to add attributes to. + /// - Returns: A new font that has the correct attributes for the capture. + func fontFor(for capture: CaptureName?, from font: NSFont) -> NSFont { + let attributes = mapCapture(capture) + guard attributes.bold || attributes.italic else { + return font + } + + let descriptor = font.fontDescriptor.withSymbolicTraits(attributes.fontDescriptorTraits) + return NSFont(descriptor: descriptor, size: font.pointSize) ?? font } } From 7e0370d3015dce84f3ae8f3bb149d5e51261418d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:04:54 -0600 Subject: [PATCH 2/6] Update Test --- Tests/CodeEditSourceEditorTests/Mock.swift | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/CodeEditSourceEditorTests/Mock.swift b/Tests/CodeEditSourceEditorTests/Mock.swift index 31c3e5377..b5aa682ad 100644 --- a/Tests/CodeEditSourceEditorTests/Mock.swift +++ b/Tests/CodeEditSourceEditorTests/Mock.swift @@ -69,22 +69,22 @@ enum Mock { static func theme() -> EditorTheme { EditorTheme( - text: .textColor, + text: EditorTheme.Attribute(color: .textColor), insertionPoint: .textColor, - invisibles: .gray, + invisibles: EditorTheme.Attribute(color: .gray), background: .textBackgroundColor, lineHighlight: .highlightColor, selection: .selectedTextColor, - keywords: .systemPink, - commands: .systemBlue, - types: .systemMint, - attributes: .systemTeal, - variables: .systemCyan, - values: .systemOrange, - numbers: .systemYellow, - strings: .systemRed, - characters: .systemRed, - comments: .systemGreen + keywords: EditorTheme.Attribute(color: .systemPink), + commands: EditorTheme.Attribute(color: .systemBlue), + types: EditorTheme.Attribute(color: .systemMint), + attributes: EditorTheme.Attribute(color: .systemTeal), + variables: EditorTheme.Attribute(color: .systemCyan), + values: EditorTheme.Attribute(color: .systemOrange), + numbers: EditorTheme.Attribute(color: .systemYellow), + strings: EditorTheme.Attribute(color: .systemRed), + characters: EditorTheme.Attribute(color: .systemRed), + comments: EditorTheme.Attribute(color: .systemGreen) ) } From 38a690c6ce44afec3264c40927699c7e2ba39e0f Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:08:17 -0600 Subject: [PATCH 3/6] Two hops this time --- .../TextViewControllerTests.swift | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/Tests/CodeEditSourceEditorTests/TextViewControllerTests.swift b/Tests/CodeEditSourceEditorTests/TextViewControllerTests.swift index 60f217a9e..ac7f4ecac 100644 --- a/Tests/CodeEditSourceEditorTests/TextViewControllerTests.swift +++ b/Tests/CodeEditSourceEditorTests/TextViewControllerTests.swift @@ -12,24 +12,7 @@ final class TextViewControllerTests: XCTestCase { var theme: EditorTheme! override func setUpWithError() throws { - theme = EditorTheme( - text: .textColor, - insertionPoint: .textColor, - invisibles: .gray, - background: .textBackgroundColor, - lineHighlight: .highlightColor, - selection: .selectedTextColor, - keywords: .systemPink, - commands: .systemBlue, - types: .systemMint, - attributes: .systemTeal, - variables: .systemCyan, - values: .systemOrange, - numbers: .systemYellow, - strings: .systemRed, - characters: .systemRed, - comments: .systemGreen - ) + theme = Mock.theme() controller = TextViewController( string: "", language: .default, From 1fddd83bbe43d0ac610863ceba0a25d431edcfbc Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:20:44 -0600 Subject: [PATCH 4/6] Adjust Font Using Font Manager --- .../Views/ContentView.swift | 2 +- .../Theme/EditorTheme.swift | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift index ea55c35b5..c6a3e4b53 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift @@ -16,7 +16,7 @@ struct ContentView: View { @State private var language: CodeLanguage = .default @State private var theme: EditorTheme = .standard - @State private var font: NSFont = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + @State private var font: NSFont = NSFont.monospacedSystemFont(ofSize: 12, weight: .medium) @AppStorage("wrapLines") private var wrapLines: Bool = true @State private var cursorPositions: [CursorPosition] = [] @AppStorage("systemCursor") private var useSystemCursor: Bool = false diff --git a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift index 8fe8b9339..c477e17db 100644 --- a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift +++ b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift @@ -23,15 +23,6 @@ public struct EditorTheme: Equatable { self.bold = bold self.italic = italic } - - var fontDescriptorTraits: NSFontDescriptor.SymbolicTraits { - switch (bold, italic) { - case (true, true): return [.bold, .italic] - case (true, false): return [.bold] - case (false, true): return [.italic] - case (false, false): return [] - } - } } public var text: Attribute @@ -125,7 +116,16 @@ public struct EditorTheme: Equatable { return font } - let descriptor = font.fontDescriptor.withSymbolicTraits(attributes.fontDescriptorTraits) - return NSFont(descriptor: descriptor, size: font.pointSize) ?? font + var font = font + + if attributes.bold { + font = NSFontManager.shared.convert(font, toHaveTrait: .boldFontMask) + } + + if attributes.italic { + font = NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) + } + + return font } } From 4182aa2772f0d9b60e1e2684e5a9c07ba17fc1ee Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:31:34 -0600 Subject: [PATCH 5/6] Convert back to regular default --- .../CodeEditSourceEditorExample/Views/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift index c6a3e4b53..ea55c35b5 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift @@ -16,7 +16,7 @@ struct ContentView: View { @State private var language: CodeLanguage = .default @State private var theme: EditorTheme = .standard - @State private var font: NSFont = NSFont.monospacedSystemFont(ofSize: 12, weight: .medium) + @State private var font: NSFont = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) @AppStorage("wrapLines") private var wrapLines: Bool = true @State private var cursorPositions: [CursorPosition] = [] @AppStorage("systemCursor") private var useSystemCursor: Bool = false From e525d0718cfc66acc8562f9540e768020f0dfda8 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:38:59 -0600 Subject: [PATCH 6/6] Public Attributes --- Sources/CodeEditSourceEditor/Theme/EditorTheme.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift index c477e17db..c44cfc96f 100644 --- a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift +++ b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift @@ -14,9 +14,9 @@ import SwiftUI public struct EditorTheme: Equatable { /// Represents attributes that can be applied to style text. public struct Attribute: Equatable, Hashable, Sendable { - let color: NSColor - let bold: Bool - let italic: Bool + public let color: NSColor + public let bold: Bool + public let italic: Bool public init(color: NSColor, bold: Bool = false, italic: Bool = false) { self.color = color