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..c44cfc96f 100644 --- a/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift +++ b/Sources/CodeEditSourceEditor/Theme/EditorTheme.swift @@ -7,43 +7,58 @@ 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 { + public let color: NSColor + public let bold: Bool + public 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 + } + } + + 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 +78,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 +97,35 @@ 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 + } + + 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 } } 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) ) } 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,