From b8aae54352ff5d463882257cdb40e87425904a7f Mon Sep 17 00:00:00 2001
From: 1024jp <1024jp@wolfrosch.com>
Date: Sat, 11 May 2024 15:48:48 +0900
Subject: [PATCH] Match theme selection color to the theme dark/light mode
---
CHANGELOG.md | 1 +
.../Resources/en.lproj/pgs/releasenotes.html | 1 +
.../Resources/ja.lproj/pgs/releasenotes.html | 1 +
CotEditor/Sources/EditorTextView.swift | 2 +-
CotEditor/Sources/LayoutManager.swift | 9 ++-
CotEditor/Sources/NSAppearance.swift | 71 ++++++++++++++++++-
CotEditor/Sources/Theme.swift | 51 ++++++++-----
CotEditor/Sources/ThemeEditorView.swift | 2 +-
8 files changed, 114 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b08869645..bd7c47c294 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
### Improvements
- Display the current editor mode in the document inspector.
+- Use more appropriate system color for the selection background when dark mode or not differs between the editor and theme.
- Improve VoiceOver support.
- Deprecate exact recalculation of insertion points when documents are updated by external processes to improve application stability.
- Improve stability of application launch behavior.
diff --git a/CotEditor/CotEditor.help/Contents/Resources/en.lproj/pgs/releasenotes.html b/CotEditor/CotEditor.help/Contents/Resources/en.lproj/pgs/releasenotes.html
index 4aba6cc33f..f492afa1a7 100644
--- a/CotEditor/CotEditor.help/Contents/Resources/en.lproj/pgs/releasenotes.html
+++ b/CotEditor/CotEditor.help/Contents/Resources/en.lproj/pgs/releasenotes.html
@@ -37,6 +37,7 @@
Improvements
- Display the current editor mode in the document inspector.
+ - Use more appropriate system color for the selection background when dark mode or not differs between the editor and theme.
- Improve VoiceOver support.
- Deprecate exact recalculation of insertion points when documents are updated by external processes to improve application stability.
- Improve stability of application launch behavior.
diff --git a/CotEditor/CotEditor.help/Contents/Resources/ja.lproj/pgs/releasenotes.html b/CotEditor/CotEditor.help/Contents/Resources/ja.lproj/pgs/releasenotes.html
index ab45e7f442..4fae642043 100644
--- a/CotEditor/CotEditor.help/Contents/Resources/ja.lproj/pgs/releasenotes.html
+++ b/CotEditor/CotEditor.help/Contents/Resources/ja.lproj/pgs/releasenotes.html
@@ -37,6 +37,7 @@ 改良
- 書類インスペクタに現在のモードを表示
+ - エディタとテーマのダークモードかどうかが異なるときにもより適切なシステムカラーを選択範囲に用いるように改良
- VoiceOverサポートを強化
- アプリケーションの安定性向上のために、外部プロセスから書類が更新されたときに新しいカーソル位置を推算する機能おを廃止
- アプリケーション起動時の安定性を向上
diff --git a/CotEditor/Sources/EditorTextView.swift b/CotEditor/Sources/EditorTextView.swift
index 126f77fc1e..7ccd0f98f2 100644
--- a/CotEditor/Sources/EditorTextView.swift
+++ b/CotEditor/Sources/EditorTextView.swift
@@ -1365,7 +1365,7 @@ class EditorTextView: NSTextView, Themable, CurrentLineHighlighting, MultiCursor
indicator.color = self.insertionPointColor
}
}
- self.selectedTextAttributes[.backgroundColor] = theme.effectiveSelectionColor
+ self.selectedTextAttributes[.backgroundColor] = theme.effectiveSelectionColor(for: self.effectiveAppearance)
(self.layoutManager as? LayoutManager)?.invisiblesColor = theme.invisibles.color
(self.window as? DocumentWindow)?.contentBackgroundColor = theme.background.color
diff --git a/CotEditor/Sources/LayoutManager.swift b/CotEditor/Sources/LayoutManager.swift
index d09ee12535..b1e7f8b211 100644
--- a/CotEditor/Sources/LayoutManager.swift
+++ b/CotEditor/Sources/LayoutManager.swift
@@ -171,12 +171,11 @@ class LayoutManager: NSLayoutManager, InvisibleDrawing, ValidationIgnorable, Lin
// -> Otherwise, `.unemphasizedSelectedContentBackgroundColor` will be used forcibly and text becomes unreadable
// when the window appearance and theme are inconsistent.
if color == .unemphasizedSelectedContentBackgroundColor, // check if inactive
- let textContainer = self.textContainer(forGlyphAt: self.glyphIndexForCharacter(at: charRange.location),
- effectiveRange: nil, withoutAdditionalLayout: true),
- let theme = (textContainer.textView as? any Themable)?.theme,
- let secondarySelectionColor = theme.secondarySelectionColor
+ let textView = self.textContainer(forGlyphAt: self.glyphIndexForCharacter(at: charRange.location),
+ effectiveRange: nil, withoutAdditionalLayout: true)?.textView,
+ let theme = (textView as? any Themable)?.theme
{
- secondarySelectionColor.setFill()
+ theme.effectiveSecondarySelectionColor(for: textView.effectiveAppearance)?.setFill()
}
super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
diff --git a/CotEditor/Sources/NSAppearance.swift b/CotEditor/Sources/NSAppearance.swift
index aa695dce05..79289fb7ff 100644
--- a/CotEditor/Sources/NSAppearance.swift
+++ b/CotEditor/Sources/NSAppearance.swift
@@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
-// © 2018-2023 1024jp
+// © 2018-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -45,4 +45,73 @@ extension NSAppearance {
false
}
}
+
+
+ /// THe corresponding appearance for the light/dark mode.
+ ///
+ /// - Parameter isDark: Whether the return value to be for dark mode or not.
+ /// - Returns: A new appearance, or `self` if no change required.
+ final func appearance(for isDark: Bool) -> Self {
+
+ if isDark == self.isDark {
+ self
+ } else {
+ Self(named: self.name.name(for: isDark)) ?? self
+ }
+ }
+}
+
+
+extension NSColor {
+
+ /// Forcibly creates the appropriate color for the given appearance.
+ ///
+ /// - Parameter appearance: The appearance to match.
+ /// - Returns: A new color.
+ func solve(for appearance: NSAppearance?) -> NSColor {
+
+ guard self.type != .componentBased else { return self }
+
+ var color = self
+ appearance?.performAsCurrentDrawingAppearance {
+ color = NSColor(cgColor: color.cgColor) ?? color
+ }
+ return color
+ }
+
+
+ /// Forcibly creates the appropriate color for the light/dark mode.
+ ///
+ /// - Parameter isDark: Whether the return value to be for dark mode or not.
+ /// - Returns: A new color.
+ func forDarkMode(_ isDark: Bool) -> NSColor {
+
+ guard self.type != .componentBased else { return self }
+
+ return self.solve(for: NSAppearance(named: isDark ? .darkAqua : .aqua)!)
+ }
+}
+
+
+private extension NSAppearance.Name {
+
+ /// The corresponding appearance name for the light/dark mode.
+ ///
+ /// - Parameter isDark: Whether the return value to be for dark mode or not.
+ /// - Returns: An appearance name.
+ func name(for isDark: Bool) -> Self {
+
+ switch self {
+ case .aqua, .darkAqua:
+ isDark ? .darkAqua : .aqua
+ case .vibrantLight, .vibrantDark:
+ isDark ? .vibrantDark : .vibrantLight
+ case .accessibilityHighContrastAqua, .accessibilityHighContrastDarkAqua:
+ isDark ? .accessibilityHighContrastDarkAqua : .accessibilityHighContrastAqua
+ case .accessibilityHighContrastVibrantLight, .accessibilityHighContrastVibrantDark:
+ isDark ? .accessibilityHighContrastVibrantDark : .accessibilityHighContrastVibrantLight
+ default:
+ self
+ }
+ }
}
diff --git a/CotEditor/Sources/Theme.swift b/CotEditor/Sources/Theme.swift
index 81a758df96..609b40c720 100644
--- a/CotEditor/Sources/Theme.swift
+++ b/CotEditor/Sources/Theme.swift
@@ -145,18 +145,6 @@ struct Theme: Equatable {
}
- /// Selection color for inactive text view.
- var secondarySelectionColor: NSColor? {
-
- guard
- !self.selection.usesSystemSetting,
- let color = self.selection.color.usingColorSpace(.genericRGB)
- else { return nil }
-
- return NSColor(calibratedWhite: color.lightnessComponent, alpha: 1.0)
- }
-
-
/// Insertion point color to use.
var effectiveInsertionPointColor: NSColor {
@@ -168,10 +156,41 @@ struct Theme: Equatable {
}
- /// Selected text background color to use.
- var effectiveSelectionColor: NSColor {
-
- self.selection.usesSystemSetting ? .selectedTextBackgroundColor : self.selection.color
+ /// Returns the selection color to use with the given appearance.
+ ///
+ /// - Parameter appearance: The current appearance of the text view to draw.
+ /// - Returns: A color.
+ func effectiveSelectionColor(for appearance: NSAppearance) -> NSColor {
+
+ if self.selection.usesSystemSetting {
+ if self.isDarkTheme == appearance.isDark {
+ .selectedTextBackgroundColor
+ } else {
+ .selectedTextBackgroundColor.solve(for: appearance.appearance(for: self.isDarkTheme))
+ }
+ } else {
+ self.selection.color
+ }
+ }
+
+
+ /// Returns the selection color to use for inactive views.
+ ///
+ /// - Parameter appearance: The current appearance of the text view to draw.
+ /// - Returns: A color.
+ func effectiveSecondarySelectionColor(for appearance: NSAppearance) -> NSColor? {
+
+ if self.selection.usesSystemSetting {
+ return if self.isDarkTheme == appearance.isDark {
+ .unemphasizedSelectedTextBackgroundColor
+ } else {
+ .unemphasizedSelectedTextBackgroundColor.solve(for: appearance.appearance(for: self.isDarkTheme))
+ }
+ } else {
+ guard let color = self.selection.color.usingColorSpace(.genericRGB) else { return nil }
+
+ return NSColor(calibratedWhite: color.lightnessComponent, alpha: 1.0)
+ }
}
diff --git a/CotEditor/Sources/ThemeEditorView.swift b/CotEditor/Sources/ThemeEditorView.swift
index dd637c1ec7..02400c9c80 100644
--- a/CotEditor/Sources/ThemeEditorView.swift
+++ b/CotEditor/Sources/ThemeEditorView.swift
@@ -64,7 +64,7 @@ struct ThemeEditorView: View {
selection: $theme.lineHighlight.binding, supportsOpacity: false)
SystemColorPicker(String(localized: "Selection:", table: "ThemeEditor"),
selection: $theme.selection,
- systemColor: Color(nsColor: .selectedTextBackgroundColor),
+ systemColor: Color(nsColor: .selectedTextBackgroundColor.forDarkMode(self.theme.isDarkTheme)),
supportsOpacity: false)
}.accessibilityElement(children: .contain)
}.accessibilityElement(children: .contain)