Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.kdroidfilter.nucleus.darkmodedetector

import io.github.kdroidfilter.nucleus.core.runtime.Platform
import io.github.kdroidfilter.nucleus.darkmodedetector.linux.LinuxPortalThemeDetector
import io.github.kdroidfilter.nucleus.darkmodedetector.mac.MacOSThemeDetector
import io.github.kdroidfilter.nucleus.darkmodedetector.windows.WindowsThemeDetector
import java.util.function.Consumer

interface IDarkModeDetector {
fun isDark(): Boolean

fun registerListener(listener: Consumer<Boolean>)

fun removeListener(listener: Consumer<Boolean>)
}

object NoopDarkModeDetector : IDarkModeDetector {
override fun isDark(): Boolean = false

override fun registerListener(listener: Consumer<Boolean>) = Unit

override fun removeListener(listener: Consumer<Boolean>) = Unit
}

public fun getPlatformDarkModeDetector(): IDarkModeDetector =
when (Platform.Current) {
Platform.MacOS -> MacOSThemeDetector
Platform.Windows -> WindowsThemeDetector
Platform.Linux -> LinuxPortalThemeDetector
else -> NoopDarkModeDetector
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package io.github.kdroidfilter.nucleus.darkmodedetector

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalInspectionMode
import io.github.kdroidfilter.nucleus.core.runtime.Platform
import io.github.kdroidfilter.nucleus.darkmodedetector.linux.isLinuxInDarkMode
import io.github.kdroidfilter.nucleus.darkmodedetector.mac.isMacOsInDarkMode
import io.github.kdroidfilter.nucleus.darkmodedetector.windows.isWindowsInDarkMode
import java.util.function.Consumer

/**
* Composable function that returns whether the system is in dark mode.
Expand All @@ -18,11 +18,26 @@ fun isSystemInDarkMode(): Boolean {
if (isInPreview) {
return isSystemInDarkTheme()
}
val darkModeDetector = remember { getPlatformDarkModeDetector() }
val darkModeState = remember { mutableStateOf(darkModeDetector.isDark()) }

return when (Platform.Current) {
Platform.MacOS -> isMacOsInDarkMode()
Platform.Windows -> isWindowsInDarkMode()
Platform.Linux -> isLinuxInDarkMode()
else -> false
DisposableEffect(Unit) {
debugln(TAG) { "Registering OS dark mode listener in Compose" }
val listener =
Consumer<Boolean> { newValue ->
debugln(TAG) { "OS dark mode updated: $newValue" }
darkModeState.value = newValue
}

darkModeDetector.registerListener(listener)

onDispose {
debugln(TAG) { "Removing OS dark mode listener in Compose" }
darkModeDetector.removeListener(listener)
}
}

return darkModeState.value
}

private const val TAG = "PlatformThemeDetectorCompose"
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package io.github.kdroidfilter.nucleus.darkmodedetector.linux

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
import java.util.function.Consumer

Expand All @@ -18,42 +15,19 @@ private const val TAG = "LinuxPortalThemeDetector"
* The detector also monitors for SettingChanged signals in real-time via a background
* D-Bus dispatch thread.
*/
internal object LinuxPortalThemeDetector {
internal object LinuxPortalThemeDetector : IDarkModeDetector {
init {
debugln(TAG) { "Initializing Linux portal theme observer via JNI" }
NativeLinuxBridge.nativeStartObserving()
}

fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark()
override fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark()

fun registerListener(listener: Consumer<Boolean>) {
override fun registerListener(listener: Consumer<Boolean>) {
NativeLinuxBridge.registerListener(listener)
}

fun removeListener(listener: Consumer<Boolean>) {
override fun removeListener(listener: Consumer<Boolean>) {
NativeLinuxBridge.removeListener(listener)
}
}

/**
* A helper composable function that returns the current Linux dark mode state
* via the XDG Desktop Portal, updating automatically when the system theme changes.
*/
@Composable
fun isLinuxInDarkMode(): Boolean {
val darkModeState = remember { mutableStateOf(LinuxPortalThemeDetector.isDark()) }
DisposableEffect(Unit) {
debugln(TAG) { "Registering Linux portal dark mode listener in Compose" }
val listener =
Consumer<Boolean> { newValue ->
debugln(TAG) { "Compose Linux portal dark mode updated: $newValue" }
darkModeState.value = newValue
}
LinuxPortalThemeDetector.registerListener(listener)
onDispose {
debugln(TAG) { "Removing Linux portal dark mode listener in Compose" }
LinuxPortalThemeDetector.removeListener(listener)
}
}
return darkModeState.value
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package io.github.kdroidfilter.nucleus.darkmodedetector.mac

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
import java.util.function.Consumer

Expand All @@ -14,42 +11,19 @@ private const val TAG = "MacOSThemeDetector"
* to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle"
* (which is "Dark" when in dark mode) from NSUserDefaults.
*/
internal object MacOSThemeDetector {
internal object MacOSThemeDetector : IDarkModeDetector {
init {
debugln(TAG) { "Initializing macOS theme observer via JNI" }
NativeDarkModeBridge.nativeStartObserving()
}

fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark()
override fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark()

fun registerListener(listener: Consumer<Boolean>) {
override fun registerListener(listener: Consumer<Boolean>) {
NativeDarkModeBridge.registerListener(listener)
}

fun removeListener(listener: Consumer<Boolean>) {
override fun removeListener(listener: Consumer<Boolean>) {
NativeDarkModeBridge.removeListener(listener)
}
}

/**
* A helper composable function that returns the current macOS dark mode state,
* updating automatically when the system theme changes.
*/
@Composable
internal fun isMacOsInDarkMode(): Boolean {
val darkModeState = remember { mutableStateOf(MacOSThemeDetector.isDark()) }
DisposableEffect(Unit) {
debugln(TAG) { "Registering macOS dark mode listener in Compose" }
val listener =
Consumer<Boolean> { newValue ->
debugln(TAG) { "Compose macOS dark mode updated: $newValue" }
darkModeState.value = newValue
}
MacOSThemeDetector.registerListener(listener)
onDispose {
debugln(TAG) { "Removing macOS dark mode listener in Compose" }
MacOSThemeDetector.removeListener(listener)
}
}
return darkModeState.value
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package io.github.kdroidfilter.nucleus.darkmodedetector.windows

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
import java.util.function.Consumer

Expand All @@ -18,45 +15,19 @@ private const val TAG = "WindowsThemeDetector"
* The detector monitors the registry for changes in real-time via a native
* background thread using RegNotifyChangeKeyValue in async mode.
*/
internal object WindowsThemeDetector {
internal object WindowsThemeDetector : IDarkModeDetector {
init {
debugln(TAG) { "Initializing Windows theme observer via JNI" }
NativeWindowsBridge.nativeStartObserving()
}

fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark()
override fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark()

fun registerListener(listener: Consumer<Boolean>) {
override fun registerListener(listener: Consumer<Boolean>) {
NativeWindowsBridge.registerListener(listener)
}

fun removeListener(listener: Consumer<Boolean>) {
override fun removeListener(listener: Consumer<Boolean>) {
NativeWindowsBridge.removeListener(listener)
}
}

/**
* Composable function that returns whether Windows is currently in dark mode.
*/
@Composable
internal fun isWindowsInDarkMode(): Boolean {
val darkModeState = remember { mutableStateOf(WindowsThemeDetector.isDark()) }

DisposableEffect(Unit) {
debugln(TAG) { "Registering Windows dark mode listener in Compose" }
val listener =
Consumer<Boolean> { newValue ->
debugln(TAG) { "Windows dark mode updated: $newValue" }
darkModeState.value = newValue
}

WindowsThemeDetector.registerListener(listener)

onDispose {
debugln(TAG) { "Removing Windows dark mode listener in Compose" }
WindowsThemeDetector.removeListener(listener)
}
}

return darkModeState.value
}
34 changes: 34 additions & 0 deletions docs/runtime/darkmode-detector.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ dependencies {

## Usage

### Manual usage

This is the core functionality - it’s totally up to you how you want to use it. Just don’t forget to dispose its callback (`removeListener`) when you don’t need it anymore.

You can use Compose states, RxJava/Kotlin, or anything else you prefer.

Here we’re using a coroutines Flow wrapper just for demonstration.

```kt
val isSystemDarkFlow: Flow<Boolean> = callbackFlow<Boolean> {
val listener: Consumer<Boolean> = { isDark: Boolean ->
trySend(isDark)
}

val darkModeDetector = getPlatformDarkModeDetector()

// emit initial value
trySend(darkModeDetector.isDark())

// listen to dark mode change events
darkModeDetector.registerListener(listener)
awaitClose {
darkModeDetector.removeListener(listener)
}
}
isSystemDarkFlow.collect {
println("dark mode changes, isDark: $it")
}
```

### In compose

There is already a composable function available out of the box, which you can use it in your compose applications.

```kotlin
@Composable
fun App() {
Expand Down
Loading