Skip to content
Open
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
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView {
zoomLevel = settings.zoomLevel,
transparent = desktop.transparent,
backgroundColor = background,
colorScheme = desktop.colorScheme,
)
}

Expand All @@ -104,6 +105,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView {
zoomLevel = settings.zoomLevel,
transparent = desktop.transparent,
backgroundColor = background,
colorScheme = desktop.colorScheme,
)
}

Expand All @@ -124,6 +126,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView {
zoomLevel = settings.zoomLevel,
transparent = desktop.transparent,
backgroundColor = background,
colorScheme = desktop.colorScheme,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

/**
Expand Down
23 changes: 23 additions & 0 deletions webview-compose/src/jvmMain/native/linux/navigation.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions webview-compose/src/jvmMain/native/macos/navigation.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions webview-compose/src/jvmMain/native/windows/navigation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICoreWebView2_13> webview13;
if (FAILED(s->webview.As(&webview13)) || !webview13) return;
ComPtr<ICoreWebView2Profile> 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) {
Expand Down