Skip to content

Commit f854d83

Browse files
authored
Merge pull request #195 from kdroidFilter/fix/macos-multicolor-accent
fix(system-color): return null accent color on macOS multicolor mode
2 parents 21c8059 + f9e4822 commit f854d83

8 files changed

Lines changed: 54 additions & 21 deletions

File tree

docs/runtime/system-color.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fun App() {
2424
// Build a dynamic color scheme from the system accent
2525
darkColorScheme(primary = accentColor)
2626
} else {
27+
// null = unsupported platform or macOS multicolor mode
2728
darkColorScheme()
2829
}
2930
) {
@@ -32,6 +33,9 @@ fun App() {
3233
}
3334
```
3435

36+
!!! note "macOS Multicolor Mode"
37+
On macOS, when the user selects **Multicolor** in System Settings → Appearance → Accent color, each app is expected to use its own default color. In this case, `systemAccentColor()` returns `null` so your app can fall back to its own brand color or a default palette.
38+
3539
### Check Support
3640

3741
Use `isSystemAccentColorSupported()` to check platform support before entering a composable context — useful for feature gating or conditional UI:
@@ -98,15 +102,15 @@ The Nucleus example app uses this exact approach — you can test it by running:
98102

99103
| Function | Returns | Description |
100104
|----------|---------|-------------|
101-
| `systemAccentColor()` | `Color?` | Composable. Returns the current system accent color, or `null` if unsupported. Recomposes on change. |
105+
| `systemAccentColor()` | `Color?` | Composable. Returns the current system accent color, or `null` if unsupported or if the OS is in multicolor mode (macOS). Recomposes on change. |
102106
| `isSystemInHighContrast()` | `Boolean` | Composable. Returns `true` if the OS is in high contrast / increased contrast mode. Recomposes on change. |
103107
| `isSystemAccentColorSupported()` | `Boolean` | Non-composable. Returns whether the current platform supports accent color detection. |
104108

105109
## Platform Detection Methods
106110

107111
| Platform | Accent Color | High Contrast | Reactive |
108112
|----------|-------------|---------------|----------|
109-
| **macOS** | `NSColor.controlAccentColor` (macOS 10.14+) | `accessibilityDisplayShouldIncreaseContrast` | Yes — `NSSystemColorsDidChangeNotification` / `NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification` |
113+
| **macOS** | `NSColor.controlAccentColor` (macOS 10.14+). Returns `null` in **multicolor mode** (detected via `AppleAccentColor` user default). | `accessibilityDisplayShouldIncreaseContrast` | Yes — `NSSystemColorsDidChangeNotification` / `NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification` |
110114
| **Windows** | `HKCU\SOFTWARE\Microsoft\Windows\DWM\AccentColor` registry key (AABBGGRR) | `SystemParametersInfoW(SPI_GETHIGHCONTRAST)` | Yes — `RegNotifyChangeKeyValue` on background thread |
111115
| **Linux** | XDG Desktop Portal `org.freedesktop.appearance` / `accent-color` — RGB tuple (0.0–1.0) | XDG Desktop Portal `org.freedesktop.appearance` / `contrast` — uint32 (1 = high) | Yes — D-Bus `SettingChanged` signal listener |
112116

@@ -142,6 +146,7 @@ When ProGuard is enabled, the native bridge classes must be preserved. The Nucle
142146
-keep class io.github.kdroidfilter.nucleus.systemcolor.mac.NativeMacSystemColorBridge {
143147
native <methods>;
144148
static void onAccentColorChanged(float, float, float);
149+
static void onAccentColorCleared();
145150
static void onContrastChanged(boolean);
146151
}
147152

example/src/main/kotlin/com/example/demo/Main.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ fun main(args: Array<String>) {
155155
ThemeMode.Light -> false
156156
}
157157
val accentColor = systemAccentColor()
158-
val seedColor = accentColor ?: Color(0xFF6750A4)
158+
val seedColor = accentColor ?: Color.Yellow
159159

160160
var isRtl by remember { mutableStateOf(false) }
161161

jewel-sample/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@
278278
-keep class io.github.kdroidfilter.nucleus.systemcolor.mac.NativeMacSystemColorBridge {
279279
native <methods>;
280280
static void onAccentColorChanged(float, float, float);
281+
static void onAccentColorCleared();
281282
static void onContrastChanged(boolean);
282283
}
283284

plugin-build/plugin/src/main/resources/default-compose-desktop-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
-keep class io.github.kdroidfilter.nucleus.systemcolor.mac.NativeMacSystemColorBridge {
182182
native <methods>;
183183
static void onAccentColorChanged(float, float, float);
184+
static void onAccentColorCleared();
184185
static void onContrastChanged(boolean);
185186
}
186187

system-color/src/main/kotlin/io/github/kdroidfilter/nucleus/systemcolor/mac/MacSystemColor.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ internal object MacSystemColorDetector {
2222

2323
fun isHighContrast(): Boolean = NativeMacSystemColorBridge.nativeIsHighContrast()
2424

25-
fun registerAccentListener(listener: Consumer<Color>) {
25+
fun registerAccentListener(listener: Consumer<Color?>) {
2626
NativeMacSystemColorBridge.registerAccentListener(listener)
2727
}
2828

29-
fun removeAccentListener(listener: Consumer<Color>) {
29+
fun removeAccentListener(listener: Consumer<Color?>) {
3030
NativeMacSystemColorBridge.removeAccentListener(listener)
3131
}
3232

@@ -44,7 +44,7 @@ internal fun macOsAccentColor(): Color? {
4444
val colorState = remember { mutableStateOf(MacSystemColorDetector.getAccentColor()) }
4545
DisposableEffect(Unit) {
4646
val listener =
47-
Consumer<Color> { newColor ->
47+
Consumer<Color?> { newColor ->
4848
colorState.value = newColor
4949
}
5050
MacSystemColorDetector.registerAccentListener(listener)

system-color/src/main/kotlin/io/github/kdroidfilter/nucleus/systemcolor/mac/NativeMacSystemColorBridge.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ private const val LIBRARY_NAME = "nucleus_systemcolor"
1212

1313
@Suppress("TooManyFunctions")
1414
internal object NativeMacSystemColorBridge {
15-
private val accentListeners: MutableSet<Consumer<Color>> = ConcurrentHashMap.newKeySet()
15+
private val accentListeners: MutableSet<Consumer<Color?>> = ConcurrentHashMap.newKeySet()
1616
private val contrastListeners: MutableSet<Consumer<Boolean>> = ConcurrentHashMap.newKeySet()
1717
private val loaded = NativeLibraryLoader.load(LIBRARY_NAME, NativeMacSystemColorBridge::class.java)
1818

@@ -59,17 +59,23 @@ internal object NativeMacSystemColorBridge {
5959
accentListeners.forEach { it.accept(color) }
6060
}
6161

62+
@JvmStatic
63+
fun onAccentColorCleared() {
64+
debugln(TAG) { "Accent color cleared (multicolor mode)" }
65+
accentListeners.forEach { it.accept(null) }
66+
}
67+
6268
@JvmStatic
6369
fun onContrastChanged(isHigh: Boolean) {
6470
debugln(TAG) { "Contrast mode changed: high=$isHigh" }
6571
contrastListeners.forEach { it.accept(isHigh) }
6672
}
6773

68-
fun registerAccentListener(listener: Consumer<Color>) {
74+
fun registerAccentListener(listener: Consumer<Color?>) {
6975
accentListeners.add(listener)
7076
}
7177

72-
fun removeAccentListener(listener: Consumer<Color>) {
78+
fun removeAccentListener(listener: Consumer<Color?>) {
7379
accentListeners.remove(listener)
7480
}
7581

system-color/src/main/native/macos/NucleusSystemColorBridge.m

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
1010
return JNI_VERSION_1_8;
1111
}
1212

13-
// Helper: call back into Kotlin with accent color RGB floats
13+
// Returns YES if the user has chosen a specific accent color, NO if multicolor mode is active.
14+
static BOOL isAccentColorSet(void) {
15+
return [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleAccentColor"] != nil;
16+
}
17+
18+
// Helper: call back into Kotlin with accent color RGB floats (or null if multicolor)
1419
static void notifyAccentColorChanged(void) {
1520
if (g_jvm == NULL) return;
1621

@@ -24,17 +29,24 @@ static void notifyAccentColorChanged(void) {
2429
return;
2530
}
2631

27-
if (@available(macOS 10.14, *)) {
28-
NSColor *color = [[NSColor controlAccentColor]
29-
colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
30-
if (color != nil) {
31-
jfloat r = (jfloat)[color redComponent];
32-
jfloat g = (jfloat)[color greenComponent];
33-
jfloat b = (jfloat)[color blueComponent];
34-
35-
jclass bridgeClass = (*env)->FindClass(env,
36-
"io/github/kdroidfilter/nucleus/systemcolor/mac/NativeMacSystemColorBridge");
37-
if (bridgeClass != NULL) {
32+
jclass bridgeClass = (*env)->FindClass(env,
33+
"io/github/kdroidfilter/nucleus/systemcolor/mac/NativeMacSystemColorBridge");
34+
if (bridgeClass != NULL) {
35+
if (!isAccentColorSet()) {
36+
// Multicolor mode: notify with null
37+
jmethodID method = (*env)->GetStaticMethodID(env,
38+
bridgeClass, "onAccentColorCleared", "()V");
39+
if (method != NULL) {
40+
(*env)->CallStaticVoidMethod(env, bridgeClass, method);
41+
}
42+
} else if (@available(macOS 10.14, *)) {
43+
NSColor *color = [[NSColor controlAccentColor]
44+
colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
45+
if (color != nil) {
46+
jfloat r = (jfloat)[color redComponent];
47+
jfloat g = (jfloat)[color greenComponent];
48+
jfloat b = (jfloat)[color blueComponent];
49+
3850
jmethodID method = (*env)->GetStaticMethodID(env,
3951
bridgeClass, "onAccentColorChanged", "(FFF)V");
4052
if (method != NULL) {
@@ -93,6 +105,10 @@ static void notifyContrastChanged(void) {
93105
Java_io_github_kdroidfilter_nucleus_systemcolor_mac_NativeMacSystemColorBridge_nativeGetAccentColor(
94106
JNIEnv *env, jclass clazz, jfloatArray out) {
95107
@autoreleasepool {
108+
// In multicolor mode, AppleAccentColor is absent — return false so Kotlin gets null
109+
if (!isAccentColorSet()) {
110+
return JNI_FALSE;
111+
}
96112
if (@available(macOS 10.14, *)) {
97113
NSColor *color = [[NSColor controlAccentColor]
98114
colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];

system-color/src/main/resources/META-INF/native-image/io.github.kdroidfilter/nucleus.system-color/reachability-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
"float",
2626
"float"
2727
]
28+
},
29+
{
30+
"name": "onAccentColorCleared",
31+
"parameterTypes": []
2832
}
2933
]
3034
},

0 commit comments

Comments
 (0)