diff --git a/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/PlatformWebSettings.kt b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/PlatformWebSettings.kt index 0c6a98d..51e3710 100644 --- a/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/PlatformWebSettings.kt +++ b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/PlatformWebSettings.kt @@ -28,6 +28,10 @@ sealed class PlatformWebSettings { var incognito: Boolean = false, var autoplayWithoutUserInteraction: Boolean = false, var focused: Boolean = true, + /** See [WebViewColorScheme]'s own doc. Applied once, at WebView creation time - like + * every other field here - call [dev.nucleusframework.webview.web.IWebView.setColorScheme] + * to change it afterward without recreating the WebView. */ + var colorScheme: WebViewColorScheme = WebViewColorScheme.SYSTEM, ) : PlatformWebSettings() data class IOSWebSettings( diff --git a/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/WebViewColorScheme.kt b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/WebViewColorScheme.kt new file mode 100644 index 0000000..b1fa40f --- /dev/null +++ b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/setting/WebViewColorScheme.kt @@ -0,0 +1,22 @@ +package dev.nucleusframework.webview.setting + +/** + * Which color scheme a [dev.nucleusframework.webview.web.IWebView] should render its loaded + * content in, independently of whether that content declares its own `color-scheme`/ + * `prefers-color-scheme` support. + * + * Only desktop (Linux/macOS/Windows) backends need this spelled out explicitly - unlike every + * other engine this library targets, none of WKWebView, WebView2, or WebKit2GTK resolves + * `prefers-color-scheme` from the system theme on its own when embedded without a full browser + * chrome around it, so content that relies on that (rather than setting every color explicitly) + * renders as light regardless of the host app's own theme unless told otherwise. Android + * (`androidx.webkit.WebSettingsCompat.setAlgorithmicDarkeningAllowed`) and iOS/Wasm already behave + * correctly without this, so [IWebView.setColorScheme] is a no-op there. + */ +enum class WebViewColorScheme { + /** Match the host OS's current light/dark setting. The default - matches this library's + * existing behavior for every caller that doesn't opt in. */ + SYSTEM, + LIGHT, + DARK, +} diff --git a/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/web/IWebView.kt b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/web/IWebView.kt index 8d39db4..aecfe7c 100644 --- a/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/web/IWebView.kt +++ b/webview-compose/src/commonMain/kotlin/dev/nucleusframework/webview/web/IWebView.kt @@ -2,6 +2,7 @@ package dev.nucleusframework.webview.web import dev.nucleusframework.webview.jsbridge.WebViewJsBridge import dev.nucleusframework.webview.jsbridge.jsBridgeObjectScript +import dev.nucleusframework.webview.setting.WebViewColorScheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -53,6 +54,14 @@ interface IWebView { callback: ((String) -> Unit)? = null ) + /** + * Changes which color scheme this WebView renders loaded content in - see + * [WebViewColorScheme]'s own doc. No-op by default; only the desktop backends (Linux/macOS/ + * Windows) override this, since every other engine this library targets already resolves + * `prefers-color-scheme` from the system theme correctly on its own. + */ + fun setColorScheme(scheme: WebViewColorScheme) {} + /** * Captures a screenshot of the WebView. * Returns a [ByteArray] containing the image data (typically PNG), or null if failed. diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/DesktopWebView.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/DesktopWebView.kt index 61a7ff9..a2cb2b7 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/DesktopWebView.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/DesktopWebView.kt @@ -1,6 +1,7 @@ package dev.nucleusframework.webview.web import dev.nucleusframework.webview.jsbridge.WebViewJsBridge +import dev.nucleusframework.webview.setting.WebViewColorScheme import dev.nucleusframework.webview.util.KLogger import dev.nucleusframework.webview.web.linux.LinuxWebKitNativeWebView import dev.nucleusframework.webview.web.macos.MacOsWebKitNativeWebView @@ -120,6 +121,14 @@ internal class DesktopWebView( } } + override fun setColorScheme(scheme: WebViewColorScheme) { + when (val native = nativeWebView) { + is LinuxWebKitNativeWebView -> native.setColorScheme(scheme) + is MacOsWebKitNativeWebView -> native.setColorScheme(scheme) + is WindowsWebView2NativeWebView -> native.setColorScheme(scheme) + } + } + override suspend fun captureScreenshotOrNull(): ByteArray? { when (val native = nativeWebView) { is LinuxWebKitNativeWebView -> return native.captureScreenshotAsync() diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/WebViewDesktop.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/WebViewDesktop.kt index 3fd4c15..1084674 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/WebViewDesktop.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/WebViewDesktop.kt @@ -89,6 +89,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView { zoomLevel = settings.zoomLevel, transparent = desktop.transparent, backgroundColor = background, + colorScheme = desktop.colorScheme, ) } @@ -104,6 +105,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView { zoomLevel = settings.zoomLevel, transparent = desktop.transparent, backgroundColor = background, + colorScheme = desktop.colorScheme, ) } @@ -124,6 +126,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView { zoomLevel = settings.zoomLevel, transparent = desktop.transparent, backgroundColor = background, + colorScheme = desktop.colorScheme, ) } diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/LinuxWebKitNativeWebView.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/LinuxWebKitNativeWebView.kt index be7d5e6..aeac984 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/LinuxWebKitNativeWebView.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/LinuxWebKitNativeWebView.kt @@ -2,6 +2,7 @@ package dev.nucleusframework.webview.web.linux import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import dev.nucleusframework.webview.setting.WebViewColorScheme import dev.nucleusframework.webview.web.NativeWebView import dev.nucleusframework.window.tao.NucleusPlatformView import kotlinx.coroutines.CompletableDeferred @@ -24,6 +25,7 @@ class LinuxWebKitNativeWebView( zoomLevel: Double = 1.0, transparent: Boolean = false, backgroundColor: Color = Color.White, + colorScheme: WebViewColorScheme = WebViewColorScheme.SYSTEM, ) : NativeWebView() { private val handle: Long private val gtkWidgetHandle: Long @@ -65,6 +67,21 @@ class LinuxWebKitNativeWebView( require(handle != 0L) { "Failed to create WebKitWebView" } gtkWidgetHandle = WebKitLinuxBridge.nativeGetGtkWidget(handle) require(gtkWidgetHandle != 0L) { "Failed to get GtkWidget handle" } + if (colorScheme != WebViewColorScheme.SYSTEM) { + setColorScheme(colorScheme) + } + } + + /** + * Sets `gtk-application-prefer-dark-theme` on this widget's own `GtkSettings` - see + * [WebViewColorScheme]'s own doc, and the native implementation's own doc for why this is + * display-wide rather than per-widget on this platform. [WebViewColorScheme.SYSTEM] restores + * `false` (GTK's own default), not necessarily "follow the OS" - there is no narrower API to + * reach for here. + */ + fun setColorScheme(scheme: WebViewColorScheme) { + if (!isReady()) return + WebKitLinuxBridge.nativeSetPreferDarkTheme(handle, scheme == WebViewColorScheme.DARK) } /** Creates the [NucleusPlatformView] used by NativeView embedding. */ diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/WebKitLinuxBridge.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/WebKitLinuxBridge.kt index 0b12119..d6f3aee 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/WebKitLinuxBridge.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/linux/WebKitLinuxBridge.kt @@ -175,6 +175,11 @@ internal object WebKitLinuxBridge { @JvmStatic external fun nativeSetZoomLevel(handle: Long, zoom: Double) + /** Sets `GtkSettings:gtk-application-prefer-dark-theme` on this widget's own display - see the + * native implementation's own doc for why this is display-wide, not per-widget. */ + @JvmStatic + external fun nativeSetPreferDarkTheme(handle: Long, preferDark: Boolean) + @JvmStatic external fun nativeFocus(handle: Long) diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/MacOsWebKitNativeWebView.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/MacOsWebKitNativeWebView.kt index 2b06ca1..662cb1a 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/MacOsWebKitNativeWebView.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/MacOsWebKitNativeWebView.kt @@ -2,6 +2,7 @@ package dev.nucleusframework.webview.web.macos import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import dev.nucleusframework.webview.setting.WebViewColorScheme import dev.nucleusframework.webview.web.NativeWebView import dev.nucleusframework.window.tao.NucleusPlatformView import kotlinx.coroutines.CompletableDeferred @@ -24,6 +25,7 @@ class MacOsWebKitNativeWebView( zoomLevel: Double = 1.0, transparent: Boolean = false, backgroundColor: Color = Color.White, + colorScheme: WebViewColorScheme = WebViewColorScheme.SYSTEM, ) : NativeWebView() { private val handle: Long private val nsViewHandle: Long @@ -64,6 +66,25 @@ class MacOsWebKitNativeWebView( require(handle != 0L) { "Failed to create WKWebView" } nsViewHandle = WebKitMacOsBridge.nativeGetNsView(handle) require(nsViewHandle != 0L) { "Failed to get NSView handle" } + if (colorScheme != WebViewColorScheme.SYSTEM) { + setColorScheme(colorScheme) + } + } + + /** + * Sets this WKWebView's own `NSAppearance`, which Chromium/WebKit resolves + * `prefers-color-scheme`/`color-scheme` against - see [WebViewColorScheme]'s own doc for why + * this is necessary at all. [WebViewColorScheme.SYSTEM] clears the override (`nil`), letting + * the view follow its window's appearance again. + */ + fun setColorScheme(scheme: WebViewColorScheme) { + if (!isReady()) return + val appearance = when (scheme) { + WebViewColorScheme.SYSTEM -> 0 + WebViewColorScheme.LIGHT -> 1 + WebViewColorScheme.DARK -> 2 + } + WebKitMacOsBridge.nativeSetAppearance(handle, appearance) } /** Creates the [NucleusPlatformView] used by NativeView embedding. */ diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/WebKitMacOsBridge.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/WebKitMacOsBridge.kt index dd216fc..2641ef0 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/WebKitMacOsBridge.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/macos/WebKitMacOsBridge.kt @@ -171,6 +171,10 @@ internal object WebKitMacOsBridge { @JvmStatic external fun nativeSetZoomLevel(handle: Long, zoom: Double) + /** `appearance`: 0 = system (clears the override), 1 = light, 2 = dark. */ + @JvmStatic + external fun nativeSetAppearance(handle: Long, appearance: Int) + @JvmStatic external fun nativeFocus(handle: Long) diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WebView2WindowsBridge.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WebView2WindowsBridge.kt index ec3b584..7d1d482 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WebView2WindowsBridge.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WebView2WindowsBridge.kt @@ -173,6 +173,10 @@ internal object WebView2WindowsBridge { @JvmStatic external fun nativeSetZoomLevel(handle: Long, zoom: Double) + /** `scheme`: 0 = auto (follow the OS), 1 = light, 2 = dark. */ + @JvmStatic + external fun nativeSetPreferredColorScheme(handle: Long, scheme: Int) + @JvmStatic external fun nativeFocus(handle: Long) diff --git a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WindowsWebView2NativeWebView.kt b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WindowsWebView2NativeWebView.kt index e74368f..7c82e00 100644 --- a/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WindowsWebView2NativeWebView.kt +++ b/webview-compose/src/jvmMain/kotlin/dev/nucleusframework/webview/web/windows/WindowsWebView2NativeWebView.kt @@ -2,6 +2,7 @@ package dev.nucleusframework.webview.web.windows import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import dev.nucleusframework.webview.setting.WebViewColorScheme import dev.nucleusframework.webview.web.NativeWebView import dev.nucleusframework.window.tao.NucleusPlatformView import kotlinx.coroutines.CompletableDeferred @@ -32,6 +33,7 @@ class WindowsWebView2NativeWebView( zoomLevel: Double = 1.0, transparent: Boolean = false, backgroundColor: Color = Color.White, + colorScheme: WebViewColorScheme = WebViewColorScheme.SYSTEM, ) : NativeWebView() { private val handle: Long private var released = false @@ -73,6 +75,25 @@ class WindowsWebView2NativeWebView( require(handle != 0L) { "Failed to create WebView2 (is WebView2 Runtime installed?)" } + if (colorScheme != WebViewColorScheme.SYSTEM) { + setColorScheme(colorScheme) + } + } + + /** + * Sets this profile's `PreferredColorScheme`, which Chromium resolves + * `prefers-color-scheme`/`color-scheme` against - see [WebViewColorScheme]'s own doc for why + * this is necessary at all. [WebViewColorScheme.SYSTEM] restores the default (`Auto`), + * following the OS again. + */ + fun setColorScheme(scheme: WebViewColorScheme) { + if (!isReady()) return + val native = when (scheme) { + WebViewColorScheme.SYSTEM -> 0 + WebViewColorScheme.LIGHT -> 1 + WebViewColorScheme.DARK -> 2 + } + WebView2WindowsBridge.nativeSetPreferredColorScheme(handle, native) } /** diff --git a/webview-compose/src/jvmMain/native/linux/navigation.c b/webview-compose/src/jvmMain/native/linux/navigation.c index 128cb96..9be7ca7 100644 --- a/webview-compose/src/jvmMain/native/linux/navigation.c +++ b/webview-compose/src/jvmMain/native/linux/navigation.c @@ -180,6 +180,29 @@ Java_dev_nucleusframework_webview_web_linux_WebKitLinuxBridge_nativeSetZoomLevel webkit_web_view_set_zoom_level(state->web_view, zoom); } +/** + * Sets the "prefer dark theme" GTK setting on this widget's own GtkSettings, which WebKitGTK + * consults when deciding what to report for `prefers-color-scheme` - see WebViewColorScheme's own + * Kotlin doc (commonMain) for why this is needed at all. + * + * Unlike macOS's NSAppearance (per-NSView) and Windows' CoreWebView2Profile (per-profile), GTK has + * no equivalent per-widget scheme override - `gtk-application-prefer-dark-theme` lives on + * GtkSettings, which is shared per GdkScreen/display, so this affects every GTK widget on that + * display's theme rendering too, not just this WebView. Best effort; there is no narrower API to + * reach for on this platform. + */ +JNIEXPORT void JNICALL +Java_dev_nucleusframework_webview_web_linux_WebKitLinuxBridge_nativeSetPreferDarkTheme( + JNIEnv *env, jclass clazz, jlong handle, jboolean prefer_dark) +{ + (void) env; (void) clazz; + ComposeWebViewState *state = compose_webview_state_from_handle(handle); + if (state == NULL || state->web_view == NULL) return; + GtkSettings *settings = gtk_widget_get_settings(GTK_WIDGET(state->web_view)); + if (settings == NULL) return; + g_object_set(settings, "gtk-application-prefer-dark-theme", prefer_dark ? TRUE : FALSE, NULL); +} + JNIEXPORT void JNICALL Java_dev_nucleusframework_webview_web_linux_WebKitLinuxBridge_nativeFocus( JNIEnv *env, jclass clazz, jlong handle) diff --git a/webview-compose/src/jvmMain/native/macos/navigation.m b/webview-compose/src/jvmMain/native/macos/navigation.m index 36a575a..1e70996 100644 --- a/webview-compose/src/jvmMain/native/macos/navigation.m +++ b/webview-compose/src/jvmMain/native/macos/navigation.m @@ -169,6 +169,33 @@ state.webView.pageZoom = zoom; } +/** + * Sets the WKWebView's own `NSAppearance`, which WebKit resolves `prefers-color-scheme`/ + * `color-scheme` against for content that doesn't set every color explicitly - see + * WebViewColorScheme's own Kotlin doc (commonMain) for why this is needed at all. + * appearance: 0 = system (nil, follow the window's own appearance), 1 = light (aqua), + * 2 = dark (darkAqua). + */ +JNIEXPORT void JNICALL +Java_dev_nucleusframework_webview_web_macos_WebKitMacOsBridge_nativeSetAppearance( + JNIEnv *env, jclass clazz, jlong handle, jint appearance) +{ + (void)env; (void)clazz; + ComposeWebViewState *state = compose_webview_state_from_handle(handle); + if (state == nil || state.webView == nil) return; + switch (appearance) { + case 1: + state.webView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + break; + case 2: + state.webView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + break; + default: + state.webView.appearance = nil; + break; + } +} + JNIEXPORT void JNICALL Java_dev_nucleusframework_webview_web_macos_WebKitMacOsBridge_nativeFocus( JNIEnv *env, jclass clazz, jlong handle) diff --git a/webview-compose/src/jvmMain/native/windows/navigation.cpp b/webview-compose/src/jvmMain/native/windows/navigation.cpp index 8f9ba6e..5deb529 100644 --- a/webview-compose/src/jvmMain/native/windows/navigation.cpp +++ b/webview-compose/src/jvmMain/native/windows/navigation.cpp @@ -202,6 +202,33 @@ Java_dev_nucleusframework_webview_web_windows_WebView2WindowsBridge_nativeSetZoo } } +/** + * Sets the WebView2 profile's preferred color scheme, which Chromium resolves + * `prefers-color-scheme`/`color-scheme` against - see WebViewColorScheme's own Kotlin doc + * (commonMain) for why this is needed at all. scheme: 0 = auto (follow the OS), 1 = light, + * 2 = dark. ICoreWebView2_13::get_Profile/ICoreWebView2Profile::put_PreferredColorScheme were + * introduced in WebView2 SDK 1.0.1264.42, well below the 1.0.2210.55 this project vendors (see + * build.bat) - written against the documented API surface, but not locally compiled/verified on + * Windows in this change; please confirm with a real Windows build before relying on it. + */ +JNIEXPORT void JNICALL +Java_dev_nucleusframework_webview_web_windows_WebView2WindowsBridge_nativeSetPreferredColorScheme( + JNIEnv *, jclass, jlong handle, jint scheme) { + ComposeWebViewState *s = compose_webview_state_from_handle(handle); + if (!s || !s->webview) return; + ComPtr webview13; + if (FAILED(s->webview.As(&webview13)) || !webview13) return; + ComPtr profile; + if (FAILED(webview13->get_Profile(&profile)) || !profile) return; + COREWEBVIEW2_PREFERRED_COLOR_SCHEME nativeScheme; + switch (scheme) { + case 1: nativeScheme = COREWEBVIEW2_PREFERRED_COLOR_SCHEME_LIGHT; break; + case 2: nativeScheme = COREWEBVIEW2_PREFERRED_COLOR_SCHEME_DARK; break; + default: nativeScheme = COREWEBVIEW2_PREFERRED_COLOR_SCHEME_AUTO; break; + } + profile->put_PreferredColorScheme(nativeScheme); +} + JNIEXPORT void JNICALL Java_dev_nucleusframework_webview_web_windows_WebView2WindowsBridge_nativeFocus( JNIEnv *, jclass, jlong handle) {