Skip to content

Commit c92c7dd

Browse files
committed
improvement(Windows/MacOS): Use actual accent color if available #1423
1 parent 1e89c10 commit c92c7dd

13 files changed

Lines changed: 249 additions & 5 deletions

File tree

common/src/desktopMain/kotlin/com/artemchep/keyguard/ui/theme/Theme.kt

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,78 @@ package com.artemchep.keyguard.ui.theme
55
import androidx.compose.material3.ColorScheme
66
import androidx.compose.material3.MaterialTheme
77
import androidx.compose.runtime.Composable
8-
import androidx.compose.runtime.NonRestartableComposable
8+
import androidx.compose.runtime.LaunchedEffect
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.rememberUpdatedState
13+
import androidx.compose.runtime.setValue
14+
import androidx.compose.ui.graphics.Color
15+
import androidx.lifecycle.Lifecycle
16+
import androidx.lifecycle.compose.LocalLifecycleOwner
17+
import com.artemchep.autotype.getSystemAccentColor as getNativeSystemAccentColor
918
import com.artemchep.keyguard.ui.LocalComposeWindow
19+
import com.artemchep.keyguard.ui.theme.m3.dynamicColorScheme
1020
import io.github.kdroidfilter.platformtools.darkmodedetector.windows.setWindowsAdaptiveTitleBar
21+
import kotlinx.coroutines.Dispatchers
22+
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.ensureActive
24+
import kotlinx.coroutines.isActive
25+
import kotlinx.coroutines.withContext
1126

1227
@Composable
13-
@NonRestartableComposable
14-
actual fun appDynamicDarkColorScheme(): ColorScheme = plainDarkColorScheme()
28+
actual fun appDynamicDarkColorScheme(): ColorScheme {
29+
val accentColor = rememberSystemAccentColor()
30+
return remember(accentColor) {
31+
if (accentColor != 0) {
32+
dynamicColorScheme(
33+
keyColor = Color(accentColor),
34+
isDark = true,
35+
)
36+
} else {
37+
plainDarkColorScheme()
38+
}
39+
}
40+
}
41+
42+
@Composable
43+
actual fun appDynamicLightColorScheme(): ColorScheme {
44+
val accentColor = rememberSystemAccentColor()
45+
return remember(accentColor) {
46+
if (accentColor != 0) {
47+
dynamicColorScheme(
48+
keyColor = Color(accentColor),
49+
isDark = false,
50+
)
51+
} else {
52+
plainLightColorScheme()
53+
}
54+
}
55+
}
1556

1657
@Composable
17-
@NonRestartableComposable
18-
actual fun appDynamicLightColorScheme(): ColorScheme = plainLightColorScheme()
58+
private fun rememberSystemAccentColor(): Int {
59+
var accentColor by remember {
60+
mutableStateOf(0)
61+
}
62+
63+
val updatedLifecycle by rememberUpdatedState(LocalLifecycleOwner.current.lifecycle)
64+
LaunchedEffect(Unit) {
65+
while (isActive) {
66+
accentColor = withContext(Dispatchers.IO) {
67+
getNativeSystemAccentColor()
68+
}
69+
70+
// We are potentially spawning processes to check the actual
71+
// accent color, so let's be conservative with the refresh rate.
72+
val delayMs = if (updatedLifecycle.currentState >= Lifecycle.State.RESUMED) {
73+
4000L
74+
} else 8000L
75+
delay(delayMs)
76+
}
77+
}
78+
return accentColor
79+
}
1980

2081
@Composable
2182
actual fun SystemUiThemeEffect() {

desktopLibJvm/src/jvmMain/kotlin/com/artemchep/autotype/DesktopLibInterop.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ internal fun DisposableScope.autoTypeOrThrow(
2323
}
2424
}
2525

26+
internal fun getSystemAccentColorOrDefault(
27+
lib: DesktopLibJna,
28+
): Int = runCatching {
29+
lib.getSystemAccentColor()
30+
}.getOrDefault(0)
31+
2632
internal fun DisposableScope.keychainAddPasswordOrThrow(
2733
lib: DesktopLibJna,
2834
id: String,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.artemchep.autotype
2+
3+
import com.artemchep.jna.DesktopLibJna
4+
5+
public fun getSystemAccentColor(): Int = runCatching {
6+
getSystemAccentColorOrDefault(
7+
lib = DesktopLibJna.get(),
8+
)
9+
}.getOrDefault(0)

desktopLibJvm/src/jvmMain/kotlin/com/artemchep/jna/DesktopLibJna.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public interface DesktopLibJna : Library {
7272

7373
public fun autoType(payload: Pointer): Boolean
7474

75+
// System accent color
76+
77+
public fun getSystemAccentColor(): Int
78+
7579
// Biometrics
7680

7781
public fun biometricsIsSupported(): Boolean

desktopLibJvm/src/jvmTest/kotlin/com/artemchep/autotype/DesktopLibInteropTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,24 @@ import kotlinx.coroutines.async
99
import kotlinx.coroutines.test.runCurrent
1010
import kotlinx.coroutines.test.runTest
1111
import kotlin.test.Test
12+
import kotlin.test.assertEquals
1213
import kotlin.test.assertFailsWith
1314
import kotlin.test.assertTrue
1415

1516
@OptIn(ExperimentalCoroutinesApi::class)
1617
class DesktopLibInteropTest {
18+
@Test
19+
fun `system accent color returns native color`() {
20+
val expected = 0xFF33_6699.toInt()
21+
val lib = FakeDesktopLibJna().apply {
22+
nativeSystemAccentColor = expected
23+
}
24+
25+
val result = getSystemAccentColorOrDefault(lib)
26+
27+
assertEquals(expected, result)
28+
}
29+
1730
@Test
1831
fun `keychain add throws when native write fails`() {
1932
val lib = FakeDesktopLibJna().apply {
@@ -125,10 +138,13 @@ class DesktopLibInteropTest {
125138
var keychainAddPasswordResult: Boolean = true
126139
var keychainGetPasswordResult: Pointer? = null
127140
var biometricsCallback: DesktopLibJna.BiometricsVerifyCallback? = null
141+
var nativeSystemAccentColor: Int = 0
128142
val freedPointers = mutableListOf<Pointer>()
129143

130144
override fun autoType(payload: Pointer): Boolean = true
131145

146+
override fun getSystemAccentColor(): Int = nativeSystemAccentColor
147+
132148
override fun biometricsIsSupported(): Boolean = true
133149

134150
override fun biometricsVerify(

desktopLibJvm/src/jvmTest/kotlin/com/artemchep/autotype/GlobalHotKeyInteropTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ class GlobalHotKeyInteropTest {
162162

163163
override fun autoType(payload: Pointer): Boolean = true
164164

165+
override fun getSystemAccentColor(): Int = 0
166+
165167
override fun biometricsIsSupported(): Boolean = true
166168

167169
override fun biometricsVerify(

desktopLibNative/src/objc/desktop_lib.m

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <stdint.h>
1212
#include <stdlib.h>
1313
#include <string.h>
14+
#include <math.h>
1415

1516
typedef void (*kg_biometrics_callback_t)(bool success, const char *error);
1617
typedef void (*kg_hotkey_callback_t)(int32_t hotkey_id);
@@ -46,6 +47,54 @@ static void kg_log_keychain_error(NSString *prefix, OSStatus status) {
4647
return [NSString stringWithUTF8String:value];
4748
}
4849

50+
static uint8_t kg_color_component_to_byte(CGFloat component) {
51+
if (!isfinite(component) || component <= 0.0) {
52+
return 0;
53+
}
54+
if (component >= 1.0) {
55+
return 255;
56+
}
57+
58+
return (uint8_t)(component * 255.0 + 0.5);
59+
}
60+
61+
int32_t kg_get_system_accent_color(void) {
62+
__block int32_t result = 0;
63+
void (^block)(void) = ^{
64+
if (![NSColor respondsToSelector:@selector(controlAccentColor)]) {
65+
result = 0;
66+
return;
67+
}
68+
69+
NSColor *accentColor = [NSColor controlAccentColor];
70+
NSColor *rgbColor = [accentColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
71+
if (rgbColor == nil) {
72+
result = 0;
73+
return;
74+
}
75+
76+
CGFloat red = 0.0;
77+
CGFloat green = 0.0;
78+
CGFloat blue = 0.0;
79+
CGFloat alpha = 0.0;
80+
[rgbColor getRed:&red green:&green blue:&blue alpha:&alpha];
81+
82+
uint32_t argb = 0xFF000000u;
83+
argb |= ((uint32_t)kg_color_component_to_byte(red)) << 16;
84+
argb |= ((uint32_t)kg_color_component_to_byte(green)) << 8;
85+
argb |= (uint32_t)kg_color_component_to_byte(blue);
86+
result = (int32_t)argb;
87+
};
88+
89+
if ([NSThread isMainThread]) {
90+
block();
91+
} else {
92+
dispatch_sync(dispatch_get_main_queue(), block);
93+
}
94+
95+
return result;
96+
}
97+
4998
static NSMutableDictionary<NSNumber *, KGHotKeyEntry *> *kg_hotkey_registry(void) {
5099
static NSMutableDictionary<NSNumber *, KGHotKeyEntry *> *registry = nil;
51100
static dispatch_once_t once_token;

desktopLibNative/src/src/accent.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use crate::platform;
2+
3+
pub(crate) fn get_system_accent_color() -> i32 {
4+
platform::accent::get_system_accent_color()
5+
}
6+
7+
#[allow(dead_code)]
8+
pub(crate) fn opaque_argb_from_rgb(red: u8, green: u8, blue: u8) -> i32 {
9+
(0xFF00_0000_u32 | ((red as u32) << 16) | ((green as u32) << 8) | blue as u32) as i32
10+
}
11+
12+
#[allow(dead_code)]
13+
pub(crate) fn force_opaque_argb(argb: u32) -> i32 {
14+
((argb & 0x00FF_FFFF) | 0xFF00_0000) as i32
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::{force_opaque_argb, opaque_argb_from_rgb};
20+
21+
#[test]
22+
fn opaque_argb_from_rgb_packs_channels() {
23+
assert_eq!(
24+
0xFF33_6699_u32 as i32,
25+
opaque_argb_from_rgb(0x33, 0x66, 0x99),
26+
);
27+
}
28+
29+
#[test]
30+
fn force_opaque_argb_replaces_alpha_channel() {
31+
assert_eq!(0xFF11_2233_u32 as i32, force_opaque_argb(0x8011_2233),);
32+
assert_eq!(0xFF11_2233_u32 as i32, force_opaque_argb(0x0011_2233),);
33+
}
34+
}

desktopLibNative/src/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(non_snake_case)]
22

3+
mod accent;
34
mod autotype;
45
mod biometrics;
56
mod ffi;
@@ -23,6 +24,13 @@ pub extern "C" fn autoType(payload: *const c_char) -> bool {
2324
})
2425
}
2526

27+
#[cfg_attr(not(test), no_mangle)]
28+
pub extern "C" fn getSystemAccentColor() -> c_int {
29+
ffi::with_ffi_boundary("getSystemAccentColor", 0, || {
30+
Ok(accent::get_system_accent_color() as c_int)
31+
})
32+
}
33+
2634
#[cfg_attr(not(test), no_mangle)]
2735
pub extern "C" fn biometricsIsSupported() -> bool {
2836
ffi::with_ffi_boundary("biometricsIsSupported", false, || {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
unsafe extern "C" {
2+
fn kg_get_system_accent_color() -> i32;
3+
}
4+
5+
pub(crate) fn get_system_accent_color() -> i32 {
6+
// SAFETY: The Objective-C shim takes no arguments, returns a plain integer,
7+
// and transfers no ownership.
8+
unsafe { kg_get_system_accent_color() }
9+
}

0 commit comments

Comments
 (0)