diff --git a/App/App/GuerkchenApp.swift b/App/App/GuerkchenApp.swift index f34a3d3..4bb7744 100644 --- a/App/App/GuerkchenApp.swift +++ b/App/App/GuerkchenApp.swift @@ -3,13 +3,13 @@ import SwiftUI @main struct GuerkchenApp: App { @State private var appState = AppState() - @State private var colors = ColorSettings() + @State private var appearance = AppearanceSettings() var body: some Scene { Window("guerkchen", id: "main") { ContentView() .environment(appState) - .environment(colors) + .environment(appearance) .frame(minWidth: 800, minHeight: 500) } .commands { @@ -37,7 +37,7 @@ struct GuerkchenApp: App { Settings { SettingsView() - .environment(colors) + .environment(appearance) } } } diff --git a/App/UI/Editor/EditorView.swift b/App/UI/Editor/EditorView.swift index 18c6210..5b2a853 100644 --- a/App/UI/Editor/EditorView.swift +++ b/App/UI/Editor/EditorView.swift @@ -2,7 +2,7 @@ import SwiftUI struct EditorView: View { @Environment(AppState.self) private var appState - @Environment(ColorSettings.self) private var colors + @Environment(AppearanceSettings.self) private var appearance var body: some View { if let document = appState.document { @@ -11,7 +11,7 @@ struct EditorView: View { ContentUnavailableView("Datei kann nicht angezeigt werden", systemImage: "doc.badge.exclamationmark", description: Text(loadError)) } else { - GherkinTextView(document: document, text: document.text, palette: colors.palette) { code in + GherkinTextView(document: document, text: document.text, theme: appearance.theme) { code in appState.project?.steps(for: code) ?? [] } } diff --git a/App/UI/Editor/GherkinTextView.swift b/App/UI/Editor/GherkinTextView.swift index b78c15e..20fedae 100644 --- a/App/UI/Editor/GherkinTextView.swift +++ b/App/UI/Editor/GherkinTextView.swift @@ -7,11 +7,9 @@ struct GherkinTextView: NSViewRepresentable { /// Nur damit der View-Wert sich ändert, wenn sich der Text des Dokuments ändert /// (z. B. nach einer externen Änderung). Der Coordinator arbeitet weiter mit `document`. let text: String - let palette: HighlightPalette + let theme: EditorTheme let stepsProvider: (String) -> [String] - static let font = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular) - func makeCoordinator() -> Coordinator { Coordinator() } func makeNSView(context: Context) -> NSScrollView { @@ -21,7 +19,6 @@ struct GherkinTextView: NSViewRepresentable { textView.isRichText = false textView.allowsUndo = true textView.usesFindBar = true - textView.font = Self.font textView.textContainerInset = NSSize(width: 8, height: 8) textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false @@ -33,12 +30,12 @@ struct GherkinTextView: NSViewRepresentable { textView.smartInsertDeleteEnabled = false context.coordinator.textView = textView context.coordinator.suggest = SuggestController(textView: textView) - context.coordinator.update(document: document, palette: palette, stepsProvider: stepsProvider) + context.coordinator.update(document: document, theme: theme, stepsProvider: stepsProvider) return scrollView } func updateNSView(_ nsView: NSScrollView, context: Context) { - context.coordinator.update(document: document, palette: palette, stepsProvider: stepsProvider) + context.coordinator.update(document: document, theme: theme, stepsProvider: stepsProvider) } /// Deterministischer Abbau: ohne das bliebe ein offenes Vorschlagspanel am Hauptfenster hängen. @@ -52,16 +49,18 @@ struct GherkinTextView: NSViewRepresentable { weak var textView: NSTextView? var suggest: SuggestController? private var document: EditorDocument? - private var palette: HighlightPalette = [:] + private var theme: EditorTheme? private var isApplying = false - func update(document: EditorDocument, palette: HighlightPalette, stepsProvider: @escaping (String) -> [String]) { + func update(document: EditorDocument, theme: EditorTheme, stepsProvider: @escaping (String) -> [String]) { suggest?.stepsProvider = stepsProvider guard let textView else { return } let documentChanged = document !== self.document - let paletteChanged = palette != self.palette + let themeChanged = theme != self.theme self.document = document - self.palette = palette + self.theme = theme + + if themeChanged { applyTheme(theme) } if documentChanged || textView.string != document.text { isApplying = true @@ -75,15 +74,37 @@ struct GherkinTextView: NSViewRepresentable { isApplying = false suggest?.hide() rehighlight() - } else if paletteChanged { + } else if themeChanged { rehighlight() } } func rehighlight() { - guard let textView, let storage = textView.textStorage else { return } - SyntaxHighlighter.highlight(storage: storage, palette: palette, font: GherkinTextView.font) - textView.typingAttributes = [.font: GherkinTextView.font, .foregroundColor: NSColor.textColor] + guard let textView, let theme, let storage = textView.textStorage else { return } + SyntaxHighlighter.highlight(storage: storage, theme: theme) + textView.typingAttributes = [.font: theme.font, .foregroundColor: theme.textColor] + } + + // MARK: - Private + + private func applyTheme(_ theme: EditorTheme) { + guard let textView else { return } + let background = theme.backgroundColor + textView.font = theme.font + textView.drawsBackground = true + textView.backgroundColor = background + textView.insertionPointColor = theme.textColor + suggest?.font = theme.font + + guard let scrollView = textView.enclosingScrollView else { return } + scrollView.drawsBackground = true + scrollView.backgroundColor = background + // Bei eigenem Hintergrund passen Scroller und Auswahlfarbe sonst nicht dazu. + if theme.customBackgroundColor != nil { + scrollView.appearance = NSAppearance(named: background.isDark ? .darkAqua : .aqua) + } else { + scrollView.appearance = nil + } } // MARK: NSTextViewDelegate diff --git a/App/UI/Editor/SuggestController.swift b/App/UI/Editor/SuggestController.swift index 08ec41d..e5ee715 100644 --- a/App/UI/Editor/SuggestController.swift +++ b/App/UI/Editor/SuggestController.swift @@ -5,6 +5,8 @@ import GuerkchenCore @MainActor final class SuggestController { var stepsProvider: (String) -> [String] = { _ in [] } + /// Die Vorschläge landen so im Editor, wie sie hier stehen – deshalb dieselbe Schrift. + var font: NSFont = .monospacedSystemFont(ofSize: 13, weight: .regular) private weak var textView: NSTextView? private let panel = SuggestPanel() @@ -118,7 +120,7 @@ final class SuggestController { currentLineLocation = lineRange.location let cursorRect = textView.firstRect(forCharacterRange: NSRange(location: selection.location, length: 0), actualRange: nil) - panel.show(suggestions, below: cursorRect, in: window) + panel.show(suggestions, font: font, below: cursorRect, in: window) } /// Ungerade Anzahl Doc-String-Begrenzer vor dieser Zeile → wir sind in einem Doc-String. diff --git a/App/UI/Editor/SuggestPanel.swift b/App/UI/Editor/SuggestPanel.swift index 90fb175..0dcb928 100644 --- a/App/UI/Editor/SuggestPanel.swift +++ b/App/UI/Editor/SuggestPanel.swift @@ -3,10 +3,14 @@ import SwiftUI import GuerkchenCore struct SuggestListView: View { - static let rowHeight: CGFloat = 24 + /// Die Zeilen wachsen mit der eingestellten Editorschrift mit. + static func rowHeight(for font: NSFont) -> CGFloat { + max(24, ceil(font.ascender - font.descender + font.leading) + 8) + } let suggestions: [Suggestion] let selectedIndex: Int + let font: NSFont let onPick: (Int) -> Void var body: some View { @@ -17,12 +21,12 @@ struct SuggestListView: View { .foregroundStyle(.secondary) .frame(width: 14) Text(suggestion.label) - .font(.system(size: 13, design: .monospaced)) + .font(Font(font)) .lineLimit(1) Spacer(minLength: 0) } .padding(.horizontal, 8) - .frame(height: Self.rowHeight) + .frame(height: Self.rowHeight(for: font)) .background(index == selectedIndex ? Color.accentColor.opacity(0.25) : Color.clear, in: RoundedRectangle(cornerRadius: 4)) .contentShape(Rectangle()) @@ -49,6 +53,7 @@ final class SuggestPanel: NSPanel { var onPick: ((Int) -> Void)? private(set) var suggestions: [Suggestion] = [] private(set) var selectedIndex = 0 + private var font: NSFont = .monospacedSystemFont(ofSize: 13, weight: .regular) private let hosting: NSHostingView var selectedSuggestion: Suggestion? { @@ -56,7 +61,9 @@ final class SuggestPanel: NSPanel { } init() { - hosting = NSHostingView(rootView: SuggestListView(suggestions: [], selectedIndex: 0, onPick: { _ in })) + hosting = NSHostingView(rootView: SuggestListView(suggestions: [], selectedIndex: 0, + font: .monospacedSystemFont(ofSize: 13, weight: .regular), + onPick: { _ in })) super.init(contentRect: NSRect(x: 0, y: 0, width: Self.width, height: 100), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false) isFloatingPanel = true @@ -72,12 +79,13 @@ final class SuggestPanel: NSPanel { override var canBecomeKey: Bool { false } override var canBecomeMain: Bool { false } - func show(_ suggestions: [Suggestion], below cursorRect: NSRect, in window: NSWindow) { + func show(_ suggestions: [Suggestion], font: NSFont, below cursorRect: NSRect, in window: NSWindow) { self.suggestions = suggestions + self.font = font selectedIndex = 0 render() - let height = CGFloat(suggestions.count) * SuggestListView.rowHeight + 8 + let height = CGFloat(suggestions.count) * SuggestListView.rowHeight(for: font) + 8 var origin = NSPoint(x: cursorRect.minX, y: cursorRect.minY - height - 2) if let screen = window.screen { let visible = screen.visibleFrame @@ -109,7 +117,7 @@ final class SuggestPanel: NSPanel { } private func render() { - hosting.rootView = SuggestListView(suggestions: suggestions, selectedIndex: selectedIndex) { [weak self] index in + hosting.rootView = SuggestListView(suggestions: suggestions, selectedIndex: selectedIndex, font: font) { [weak self] index in self?.selectedIndex = index self?.onPick?(index) } diff --git a/App/UI/Editor/SyntaxHighlighter.swift b/App/UI/Editor/SyntaxHighlighter.swift index d944a3c..451a53a 100644 --- a/App/UI/Editor/SyntaxHighlighter.swift +++ b/App/UI/Editor/SyntaxHighlighter.swift @@ -3,16 +3,16 @@ import GuerkchenCore enum SyntaxHighlighter { @MainActor - static func highlight(storage: NSTextStorage, palette: HighlightPalette, font: NSFont) { + static func highlight(storage: NSTextStorage, theme: EditorTheme) { let text = storage.string let dialect = LineScanner.dialect(for: text) let full = NSRange(location: 0, length: storage.length) storage.beginEditing() - storage.addAttributes([.font: font, .foregroundColor: NSColor.textColor], range: full) + storage.addAttributes([.font: theme.font, .foregroundColor: theme.textColor], range: full) for line in LineScanner.scan(text, dialect: dialect) { guard let category = HighlightCategory.forLine(line.kind) else { continue } - let color = NSColor(hex: palette[category] ?? category.defaultHex) ?? .textColor + let color = theme.color(for: category) let range: Range if case let .keyword(_, keywordRange, _) = line.kind { range = keywordRange diff --git a/App/UI/Settings/SettingsView.swift b/App/UI/Settings/SettingsView.swift index 7f10964..6613cd3 100644 --- a/App/UI/Settings/SettingsView.swift +++ b/App/UI/Settings/SettingsView.swift @@ -1,22 +1,81 @@ +import AppKit import SwiftUI struct SettingsView: View { - @Environment(ColorSettings.self) private var colors + @Environment(AppearanceSettings.self) private var appearance + /// Die Schriftliste zeigt normalerweise nur Monospace-Familien – für einen Editor die + /// sinnvolle Auswahl, aber nicht jeder will sich darauf festlegen lassen. + @AppStorage("fontPickerShowsAllFonts") private var showsAllFonts = false var body: some View { Form { + Section("Schrift") { + Picker("Schriftart", selection: appearance.fontFamilyBinding) { + Text("Systemschrift (Monospace)").tag(String?.none) + Divider() + ForEach(fontFamilies, id: \.self) { family in + Text(family).tag(String?.some(family)) + } + } + Toggle("Alle Schriften anzeigen", isOn: $showsAllFonts) + Stepper("Schriftgröße: \(Int(appearance.theme.fontSize)) pt", + value: appearance.fontSizeBinding, + in: AppearanceSettings.fontSizeRange, + step: 1) + } + + Section("Hintergrund") { + Toggle("Systemhintergrund verwenden", isOn: appearance.usesSystemBackgroundBinding) + ColorPicker("Hintergrundfarbe", selection: appearance.backgroundBinding, supportsOpacity: false) + .disabled(appearance.usesSystemBackground) + } + Section("Farben der Schlüsselwörter") { ForEach(HighlightCategory.allCases) { category in - ColorPicker(category.title, selection: colors.binding(for: category), supportsOpacity: false) + ColorPicker(category.title, selection: appearance.binding(for: category), supportsOpacity: false) } } + + Section("Vorschau") { + preview + } + Section { - Button("Auf Standardfarben zurücksetzen") { colors.reset() } + Button("Auf Standardfarben zurücksetzen") { appearance.resetColors() } + Button("Schrift und Hintergrund zurücksetzen") { appearance.resetFontAndBackground() } } } .formStyle(.grouped) - .frame(width: 420) - .padding(.bottom) - .fixedSize(horizontal: false, vertical: true) + // Feste Größe: die Liste ist zu lang geworden, um das Fenster mitwachsen zu lassen. + .frame(width: 440, height: 560) + } + + private var preview: some View { + let theme = appearance.theme + return VStack(alignment: .leading, spacing: 2) { + Text("Szenario: Anmeldung") + .foregroundStyle(Color(nsColor: theme.color(for: .scenario))) + Text(" Angenommen ein Benutzer ist angemeldet") + .foregroundStyle(Color(nsColor: theme.color(for: .step))) + Text(" # ein Kommentar") + .foregroundStyle(Color(nsColor: theme.color(for: .comment))) + } + .font(Font(theme.font)) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(Color(nsColor: theme.backgroundColor), in: RoundedRectangle(cornerRadius: 6)) + .overlay(RoundedRectangle(cornerRadius: 6).stroke(.separator)) + } + + private var fontFamilies: [String] { + var families = showsAllFonts ? FontCatalog.allFamilies : FontCatalog.monospacedFamilies + // Eine zuvor gewählte Proportionalschrift darf nicht aus der Liste fallen, + // sonst stünde der Picker leer da. + if let current = appearance.theme.fontFamily, !families.contains(current) { + families.append(current) + families.sort() + } + return families } } diff --git a/App/UI/Theme/AppearanceSettings.swift b/App/UI/Theme/AppearanceSettings.swift new file mode 100644 index 0000000..128c5b4 --- /dev/null +++ b/App/UI/Theme/AppearanceSettings.swift @@ -0,0 +1,150 @@ +import AppKit +import Observation +import SwiftUI + +typealias HighlightPalette = [HighlightCategory: String] + +@MainActor +@Observable +final class AppearanceSettings { + private static let colorKeyPrefix = "highlightColor." + private static let backgroundKey = "editorBackgroundColor" + private static let fontFamilyKey = "editorFontFamily" + private static let fontSizeKey = "editorFontSize" + + static let defaultFontSize: Double = 13 + static let fontSizeRange: ClosedRange = 9...32 + + private(set) var theme = EditorTheme(palette: [:], backgroundHex: nil, fontFamily: nil, + fontSize: AppearanceSettings.defaultFontSize) + @ObservationIgnored private let defaults: UserDefaults + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + load() + } + + // MARK: - Farben + + func color(for category: HighlightCategory) -> NSColor { + theme.color(for: category) + } + + func setColor(_ color: NSColor, for category: HighlightCategory) { + let hex = color.hexString + theme.palette[category] = hex + defaults.set(hex, forKey: Self.colorKeyPrefix + category.rawValue) + } + + func binding(for category: HighlightCategory) -> Binding { + Binding( + get: { Color(nsColor: self.color(for: category)) }, + set: { self.setColor(NSColor($0), for: category) }) + } + + func resetColors() { + for category in HighlightCategory.allCases { + defaults.removeObject(forKey: Self.colorKeyPrefix + category.rawValue) + } + load() + } + + // MARK: - Hintergrund + + var usesSystemBackground: Bool { theme.backgroundHex == nil } + + func setBackgroundHex(_ hex: String?) { + theme.backgroundHex = hex + if let hex { + defaults.set(hex, forKey: Self.backgroundKey) + } else { + defaults.removeObject(forKey: Self.backgroundKey) + } + } + + var backgroundBinding: Binding { + Binding( + get: { Color(nsColor: self.theme.backgroundColor) }, + set: { self.setBackgroundHex(NSColor($0).hexString) }) + } + + var usesSystemBackgroundBinding: Binding { + Binding( + get: { self.usesSystemBackground }, + // Beim Wechsel auf eine eigene Farbe mit dem aktuellen Systemhintergrund starten, + // damit der Editor nicht plötzlich anders aussieht. + set: { self.setBackgroundHex($0 ? nil : Self.systemBackgroundHex()) }) + } + + // MARK: - Schrift + + func setFontFamily(_ family: String?) { + theme.fontFamily = family + if let family { + defaults.set(family, forKey: Self.fontFamilyKey) + } else { + defaults.removeObject(forKey: Self.fontFamilyKey) + } + } + + func setFontSize(_ size: Double) { + let clamped = min(max(size.rounded(), Self.fontSizeRange.lowerBound), Self.fontSizeRange.upperBound) + theme.fontSize = clamped + defaults.set(clamped, forKey: Self.fontSizeKey) + } + + var fontFamilyBinding: Binding { + Binding( + get: { self.theme.fontFamily }, + set: { self.setFontFamily($0) }) + } + + var fontSizeBinding: Binding { + Binding( + get: { self.theme.fontSize }, + set: { self.setFontSize($0) }) + } + + func resetFontAndBackground() { + defaults.removeObject(forKey: Self.fontFamilyKey) + defaults.removeObject(forKey: Self.fontSizeKey) + defaults.removeObject(forKey: Self.backgroundKey) + load() + } + + // MARK: - Private + + private func load() { + var palette: HighlightPalette = [:] + for category in HighlightCategory.allCases { + if let stored = defaults.string(forKey: Self.colorKeyPrefix + category.rawValue), NSColor(hex: stored) != nil { + palette[category] = stored + } else { + palette[category] = category.defaultHex + } + } + + var backgroundHex: String? + if let stored = defaults.string(forKey: Self.backgroundKey), NSColor(hex: stored) != nil { + backgroundHex = stored + } + + var fontFamily = defaults.string(forKey: Self.fontFamilyKey) + if fontFamily?.isEmpty == true { fontFamily = nil } + + let storedSize = defaults.double(forKey: Self.fontSizeKey) + let fontSize = Self.fontSizeRange.contains(storedSize) ? storedSize : Self.defaultFontSize + + theme = EditorTheme(palette: palette, backgroundHex: backgroundHex, fontFamily: fontFamily, fontSize: fontSize) + } + + /// `NSColor.textBackgroundColor` ist eine dynamische Farbe – sie muss im aktuellen + /// Erscheinungsbild aufgelöst werden, sonst käme im Dunkelmodus die helle Variante heraus. + private static func systemBackgroundHex() -> String { + var hex = "#FFFFFF" + NSApplication.shared.effectiveAppearance.performAsCurrentDrawingAppearance { + if let resolved = NSColor.textBackgroundColor.usingColorSpace(.sRGB) { hex = resolved.hexString } + } + return hex + } +} diff --git a/App/UI/Theme/ColorSettings.swift b/App/UI/Theme/ColorSettings.swift deleted file mode 100644 index fc19d8f..0000000 --- a/App/UI/Theme/ColorSettings.swift +++ /dev/null @@ -1,54 +0,0 @@ -import AppKit -import Observation -import SwiftUI - -typealias HighlightPalette = [HighlightCategory: String] - -@MainActor -@Observable -final class ColorSettings { - private static let keyPrefix = "highlightColor." - - private(set) var palette: HighlightPalette = [:] - @ObservationIgnored private let defaults: UserDefaults - - init(defaults: UserDefaults = .standard) { - self.defaults = defaults - load() - } - - func color(for category: HighlightCategory) -> NSColor { - NSColor(hex: palette[category] ?? category.defaultHex) ?? .textColor - } - - func setColor(_ color: NSColor, for category: HighlightCategory) { - let hex = color.hexString - palette[category] = hex - defaults.set(hex, forKey: Self.keyPrefix + category.rawValue) - } - - func reset() { - for category in HighlightCategory.allCases { - defaults.removeObject(forKey: Self.keyPrefix + category.rawValue) - } - load() - } - - func binding(for category: HighlightCategory) -> Binding { - Binding( - get: { Color(nsColor: self.color(for: category)) }, - set: { self.setColor(NSColor($0), for: category) }) - } - - private func load() { - var result: HighlightPalette = [:] - for category in HighlightCategory.allCases { - if let stored = defaults.string(forKey: Self.keyPrefix + category.rawValue), NSColor(hex: stored) != nil { - result[category] = stored - } else { - result[category] = category.defaultHex - } - } - palette = result - } -} diff --git a/App/UI/Theme/EditorTheme.swift b/App/UI/Theme/EditorTheme.swift new file mode 100644 index 0000000..82e31dc --- /dev/null +++ b/App/UI/Theme/EditorTheme.swift @@ -0,0 +1,43 @@ +import AppKit + +/// Alles, was das Aussehen des Editors bestimmt – als reiner Wert, damit sich billig +/// vergleichen lässt, ob neu gezeichnet werden muss. +struct EditorTheme: Equatable { + var palette: HighlightPalette + /// `nil` = Systemhintergrund, der Hell und Dunkel selbst folgt. + var backgroundHex: String? + /// `nil` = Systemschrift (Monospace). + var fontFamily: String? + var fontSize: Double + + var font: NSFont { + let size = CGFloat(fontSize) + // Gewicht 5 ist der reguläre Schnitt der Familie; fehlt die Schrift (deinstalliert, + // Projekt auf einem anderen Rechner geöffnet), bleibt es bei der Systemschrift. + guard let fontFamily, + let resolved = NSFontManager.shared.font(withFamily: fontFamily, traits: [], weight: 5, size: size) + else { return NSFont.monospacedSystemFont(ofSize: size, weight: .regular) } + return resolved + } + + /// Die gewählte Hintergrundfarbe, oder `nil`, solange der Systemhintergrund gilt. + var customBackgroundColor: NSColor? { + guard let backgroundHex else { return nil } + return NSColor(hex: backgroundHex) + } + + var backgroundColor: NSColor { + customBackgroundColor ?? .textBackgroundColor + } + + /// `.textColor` folgt dem System-Erscheinungsbild und wäre auf einem eigenen Hintergrund + /// schnell unlesbar – deshalb Schwarz oder Weiß passend zu dessen Helligkeit. + var textColor: NSColor { + guard let custom = customBackgroundColor else { return .textColor } + return custom.isDark ? .white : .black + } + + func color(for category: HighlightCategory) -> NSColor { + NSColor(hex: palette[category] ?? category.defaultHex) ?? textColor + } +} diff --git a/App/UI/Theme/FontCatalog.swift b/App/UI/Theme/FontCatalog.swift new file mode 100644 index 0000000..fdeb1e1 --- /dev/null +++ b/App/UI/Theme/FontCatalog.swift @@ -0,0 +1,16 @@ +import AppKit + +/// Die Schriftfamilien für die Auswahl in den Einstellungen. Die Abfrage beim NSFontManager +/// ist teuer genug, um sie nur einmal zu machen. +enum FontCatalog { + static let monospacedFamilies: [String] = { + let names = NSFontManager.shared.availableFontNames(with: .fixedPitchFontMask) ?? [] + var families = Set() + for name in names { + if let family = NSFont(name: name, size: 12)?.familyName { families.insert(family) } + } + return families.sorted() + }() + + static let allFamilies: [String] = NSFontManager.shared.availableFontFamilies.sorted() +} diff --git a/App/UI/Theme/NSColor+Hex.swift b/App/UI/Theme/NSColor+Hex.swift index 552d260..11f6400 100644 --- a/App/UI/Theme/NSColor+Hex.swift +++ b/App/UI/Theme/NSColor+Hex.swift @@ -19,4 +19,11 @@ extension NSColor { let b = Int(round(c.blueComponent * 255)) return String(format: "#%02X%02X%02X", r, g, b) } + + /// Helligkeit nach Rec. 601 – entscheidet, ob heller oder dunkler Text darauf lesbar ist. + var isDark: Bool { + guard let c = usingColorSpace(.sRGB) ?? usingColorSpace(.deviceRGB) else { return false } + let brightness = 0.299 * c.redComponent + 0.587 * c.greenComponent + 0.114 * c.blueComponent + return brightness < 0.55 + } } diff --git a/README.md b/README.md index b90a352..2a7bfcc 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ them: into a prompt, into a repository, or into a real Cucumber setup. - **Syntax highlighting** for eight categories: Feature/Rule, Background, Scenario/Outline/Examples, steps, comments, tags, tables and doc strings — every color configurable in Settings, with a reset to the defaults. +- **Appearance in Settings.** Font family (monospaced families by default, every installed font on + request), font size and the editor background color — with a live preview and a reset. The + background follows the system appearance until you pick a color of your own. - **Autosave.** About a second after the last edit, when switching files, and on quit. - **Recent projects.** The last project reopens on launch; the last ten are listed in the menu. - **Sandboxed**, with no network access and no third-party dependencies.