diff --git a/CoreEditor/src/@light/index.ts b/CoreEditor/src/@light/index.ts index 605aeaca2..c196a530f 100644 --- a/CoreEditor/src/@light/index.ts +++ b/CoreEditor/src/@light/index.ts @@ -21,6 +21,14 @@ window.config = config; const theme = new Compartment; window.dynamics = { theme }; +// Drive the theme from the system color scheme; the preview extension has no other source +const colorSchemeQuery = matchMedia('(prefers-color-scheme: dark)'); +const initialTheme = preferredTheme(); + +colorSchemeQuery.addEventListener('change', () => { + setTheme(preferredTheme()); +}); + const extensions = [ // Basic highlightSpecialChars(), @@ -39,7 +47,7 @@ const extensions = [ // Styling classHighlighters, - theme.of(loadTheme(config.theme)), + theme.of(initialTheme), renderExtensions, linkStyles, ]; @@ -48,7 +56,7 @@ const doc = config.text; const parent = document.querySelector('#editor') ?? document.body; window.editor = new EditorView({ doc, parent, extensions }); -setUp(config, loadTheme(config.theme).colors); +setUp(config, initialTheme.colors); // Makes sure the content doesn't have unwanted inset scrollIntoView(0); @@ -59,10 +67,6 @@ scrollIntoView(0); const bridge = window as any; const storage: { scrollbarOffset?: number } = {}; -bridge.setTheme = (name: string) => { - setTheme(loadTheme(name)); -}; - bridge.startDragging = (original: number) => { // scrollbarOffset is the distance between the top of the scrollbar and the mouse location const location = convertToLocal(original); @@ -90,14 +94,8 @@ bridge.cancelDragging = () => { // Zoom in and out using the trackpad enablePinchZoom(bridge as PinchZoomBridge); -// There're only two themes in the preview extension, -// use a simplified "loadTheme" to avoid bundling unused themes. -function loadTheme(name: string) { - if (name === 'github-dark') { - return GitHubDark(); - } else { - return GitHubLight(); - } +function preferredTheme() { + return colorSchemeQuery.matches ? GitHubDark() : GitHubLight(); } function scrollerElement(): HTMLElement | null { diff --git a/MarkEditCore/Sources/Extensions/UserDefaults+Extension.swift b/MarkEditCore/Sources/Extensions/UserDefaults+Extension.swift new file mode 100644 index 000000000..4fd08d0fe --- /dev/null +++ b/MarkEditCore/Sources/Extensions/UserDefaults+Extension.swift @@ -0,0 +1,43 @@ +// +// UserDefaults+Extension.swift +// +// Created by cyan on 6/17/26. +// + +import Foundation + +public enum ForcedColorScheme: String { + case system + case light + case dark +} + +public extension UserDefaults { + static var forcedColorScheme: ForcedColorScheme { + get { + guard let value = appGroup?.string(forKey: forcedColorSchemeKey) else { + return .system + } + + return ForcedColorScheme(rawValue: value) ?? .system + } + set { + guard newValue != .system else { + appGroup?.removeObject(forKey: forcedColorSchemeKey) + return + } + + appGroup?.set(newValue.rawValue, forKey: forcedColorSchemeKey) + } + } +} + +// MARK: - Private + +private extension UserDefaults { + static var appGroup: UserDefaults? { + UserDefaults(suiteName: "group.app.cyan.markedit") + } + + static let forcedColorSchemeKey = "forcedColorScheme" +} diff --git a/MarkEditMac/Sources/Main/AppDocumentController.swift b/MarkEditMac/Sources/Main/AppDocumentController.swift index 0ae6811f5..2f542b13b 100644 --- a/MarkEditMac/Sources/Main/AppDocumentController.swift +++ b/MarkEditMac/Sources/Main/AppDocumentController.swift @@ -6,6 +6,7 @@ // import AppKit +import MarkEditCore import MarkEditKit /** @@ -45,6 +46,18 @@ final class AppDocumentController: NSDocumentController { openPanel.showsHiddenFiles = AppPreferences.General.showHiddenFiles openPanel.relayoutAccessoryView() + let appearanceObservation = NSApp.observe(\.effectiveAppearance) { [weak self] _, _ in + Task { @MainActor in + self?.appearanceDidChange() + } + } + + defer { + appearanceObservation.invalidate() + UserDefaults.forcedColorScheme = .system + } + + appearanceDidChange() return await super.beginOpenPanel(openPanel, forTypes: inTypes) } @@ -85,6 +98,19 @@ final class AppDocumentController: NSDocumentController { // MARK: - Private +private extension AppDocumentController { + func appearanceDidChange() { + switch AppPreferences.General.appearance { + case .system: + UserDefaults.forcedColorScheme = .system + case .light: + UserDefaults.forcedColorScheme = .light + case .dark: + UserDefaults.forcedColorScheme = .dark + } + } +} + private extension NSOpenPanel { /// Re-layouts the accessory view to work around internal AppKit bugs. /// diff --git a/PreviewExtension/PreviewViewConfig.swift b/PreviewExtension/PreviewViewConfig.swift index fddb45535..d293112d6 100644 --- a/PreviewExtension/PreviewViewConfig.swift +++ b/PreviewExtension/PreviewViewConfig.swift @@ -69,10 +69,10 @@ extension WKWebViewConfiguration { } extension EditorConfig { - static func previewConfig(fileData: Data, theme: String) -> Self { + static func previewConfig(fileData: Data) -> Self { .init( text: fileData.toString() ?? "", - theme: theme, + theme: "github-light", // Ignored by @light editor fontFace: WebFontFace(family: "ui-monospace", weight: nil, style: nil), fontSize: 12, showLineNumbers: false, diff --git a/PreviewExtension/PreviewViewController+UI.swift b/PreviewExtension/PreviewViewController+UI.swift index ff7defcac..78474ba5c 100644 --- a/PreviewExtension/PreviewViewController+UI.swift +++ b/PreviewExtension/PreviewViewController+UI.swift @@ -6,33 +6,42 @@ // import AppKit +import MarkEditCore extension PreviewViewController { - var isDarkMode: Bool { - switch NSApp.effectiveAppearance.name { - case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: - return true - default: - return false - } - } - var isRightToLeft: Bool { view.userInterfaceLayoutDirection == .rightToLeft } - var effectiveTheme: String { - isDarkMode ? "github-dark" : "github-light" + func updateAppearance() { + if isDarkMode { + view.layer?.backgroundColor = NSColor(red: 13.0 / 255, green: 17.0 / 255, blue: 22.0 / 255, alpha: 1).cgColor + webView.appearance = NSAppearance(named: .darkAqua) + } else { + view.layer?.backgroundColor = NSColor.white.cgColor + webView.appearance = NSAppearance(named: .aqua) + } } +} - func updateBackgroundColor() { - // To hide the transparent background of the scrolling overflow - view.layer?.backgroundColor = (isDarkMode ? NSColor(red: 13.0 / 255, green: 17.0 / 255, blue: 22.0 / 255, alpha: 1) : NSColor.white).cgColor - } +// MARK: - Private - func updateEditorTheme() { - // To keep the app size smaller, we don't have bridge here, - // construct script literals directly. - webView.evaluateJavaScript("setTheme(`\(effectiveTheme)`)") +private extension PreviewViewController { + var isDarkMode: Bool { + switch UserDefaults.forcedColorScheme { + case .dark: + return true + case .light: + return false + case .system: + break + } + + switch NSApp.effectiveAppearance.name { + case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: + return true + default: + return false + } } } diff --git a/PreviewExtension/PreviewViewController.swift b/PreviewExtension/PreviewViewController.swift index e446d2363..90919d245 100644 --- a/PreviewExtension/PreviewViewController.swift +++ b/PreviewExtension/PreviewViewController.swift @@ -71,7 +71,7 @@ final class PreviewViewController: NSViewController { view.layer?.cornerRadius = 6 addEventMonitorsForDragging() - updateBackgroundColor() + updateAppearance() appearanceObservation = NSApp.observe(\.effectiveAppearance) { [weak self] _, _ in guard let self else { @@ -79,8 +79,7 @@ final class PreviewViewController: NSViewController { } Task { @MainActor in - self.updateBackgroundColor() - self.updateEditorTheme() + self.updateAppearance() } } } @@ -103,8 +102,7 @@ extension PreviewViewController: QLPreviewingController { previewDirectoryURL = fileURL.deletingLastPathComponent() let config = EditorConfig.previewConfig( - fileData: try Data(contentsOf: fileURL), - theme: effectiveTheme + fileData: try Data(contentsOf: fileURL) ) let html = ([config.toHtml] + userStyles).joined(separator: "\n\n")