diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index cc3b698a3..f9e5305c6 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -428,6 +428,7 @@ val windowsPlayerBridgeCommand = if (missingWindowsPlayerBridgeInputs.isNotEmpty "Ole32.lib", "User32.lib", "Gdi32.lib", + "Dwmapi.lib", ).joinToString(" ") val powershellCompileCommand = compileCommand.replace("\"", "__DQ__") val powershellCommand = """ diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt index 413e74412..fd0f9fded 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/Main.kt @@ -1,6 +1,7 @@ package com.nuvio.app import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -9,6 +10,7 @@ import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application import androidx.compose.ui.unit.dp import com.nuvio.app.features.player.PlatformPlayerSurface +import com.nuvio.app.features.player.desktop.applyNativeDesktopWindowChrome import com.nuvio.app.features.player.desktop.preloadNativePlayerBridgeAsync import java.awt.Color as AwtColor import javax.swing.JComponent @@ -40,6 +42,9 @@ fun main() { window.contentPane.background = NuvioDesktopNativeBackground (window.contentPane as? JComponent)?.isOpaque = true } + LaunchedEffect(window) { + applyNativeDesktopWindowChrome(window) + } if (smokePlayerUrl == null) { App() diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/DesktopWindowChrome.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/DesktopWindowChrome.kt new file mode 100644 index 000000000..342962590 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/DesktopWindowChrome.kt @@ -0,0 +1,21 @@ +package com.nuvio.app.features.player.desktop + +import java.awt.Window + +private const val NuvioWindowBackgroundRgb = 0x0D0D0D +private const val NuvioWindowTextRgb = 0xF5F7F8 + +internal fun applyNativeDesktopWindowChrome(window: Window) { + if (DesktopHostOs.current != DesktopHostOs.WINDOWS || !window.isDisplayable) return + + runCatching { + val hwnd = AwtNativeViewResolver.resolveNativeViewPointer(window) + NativePlayerBridge.applyWindowChrome( + windowHwnd = hwnd, + darkMode = true, + captionColorRgb = NuvioWindowBackgroundRgb, + borderColorRgb = NuvioWindowBackgroundRgb, + textColorRgb = NuvioWindowTextRgb, + ) + } +} diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerBridge.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerBridge.kt index 81a62b1b1..2665f7dba 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerBridge.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/desktop/NativePlayerBridge.kt @@ -47,6 +47,14 @@ internal object NativePlayerBridge { external fun addSubtitleUrl(handle: Long, url: String) external fun clearExternalSubtitles(handle: Long) external fun clearExternalSubtitlesAndSelect(handle: Long, trackId: Int) + external fun applyWindowChrome( + windowHwnd: Long, + darkMode: Boolean, + captionColorRgb: Int, + borderColorRgb: Int, + textColorRgb: Int, + ) + external fun setSubtitleDelayMs(handle: Long, delayMs: Int) external fun applySubtitleStyle( handle: Long, diff --git a/composeApp/src/desktopMain/native/windows/player_bridge.cpp b/composeApp/src/desktopMain/native/windows/player_bridge.cpp index ec7c5cf8c..187585e96 100644 --- a/composeApp/src/desktopMain/native/windows/player_bridge.cpp +++ b/composeApp/src/desktopMain/native/windows/player_bridge.cpp @@ -2,6 +2,7 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include #include #include #include @@ -59,6 +60,11 @@ constexpr UINT WM_NUVIO_TASK = WM_APP + 0x4E50; constexpr UINT_PTR NUVIO_TIMER_ID = 0x4E50; const wchar_t *kMessageWindowClass = L"NuvioPlayerBridgeMessageWindow"; const wchar_t *kContainerWindowClass = L"NuvioPlayerBridgeContainerWindow"; +constexpr DWORD kDwmwaUseImmersiveDarkMode = 20; +constexpr DWORD kDwmwaUseImmersiveDarkModeLegacy = 19; +constexpr DWORD kDwmwaBorderColor = 34; +constexpr DWORD kDwmwaCaptionColor = 35; +constexpr DWORD kDwmwaTextColor = 36; std::wstring toWide(const std::string &value) { if (value.empty()) return std::wstring(); @@ -131,6 +137,36 @@ std::string lowerCopy(std::string value) { return value; } +COLORREF rgbIntToColorRef(jint rgb) { + BYTE red = (BYTE)((rgb >> 16) & 0xFF); + BYTE green = (BYTE)((rgb >> 8) & 0xFF); + BYTE blue = (BYTE)(rgb & 0xFF); + return RGB(red, green, blue); +} + +void setDwmWindowAttribute(HWND hwnd, DWORD attribute, const void *value, DWORD valueSize) { + (void)DwmSetWindowAttribute(hwnd, attribute, value, valueSize); +} + +void applyDwmWindowChrome(HWND hwnd, bool darkMode, COLORREF captionColor, COLORREF borderColor, COLORREF textColor) { + if (!hwnd || !IsWindow(hwnd)) return; + + BOOL enabled = darkMode ? TRUE : FALSE; + HRESULT darkModeResult = DwmSetWindowAttribute( + hwnd, + kDwmwaUseImmersiveDarkMode, + &enabled, + sizeof(enabled) + ); + if (FAILED(darkModeResult)) { + setDwmWindowAttribute(hwnd, kDwmwaUseImmersiveDarkModeLegacy, &enabled, sizeof(enabled)); + } + + setDwmWindowAttribute(hwnd, kDwmwaCaptionColor, &captionColor, sizeof(captionColor)); + setDwmWindowAttribute(hwnd, kDwmwaBorderColor, &borderColor, sizeof(borderColor)); + setDwmWindowAttribute(hwnd, kDwmwaTextColor, &textColor, sizeof(textColor)); +} + bool containsCaseInsensitive(const std::string &haystack, const std::string &needle) { return lowerCopy(haystack).find(lowerCopy(needle)) != std::string::npos; } @@ -1654,6 +1690,25 @@ Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_clearExternalSubti if (player) player->removeExternalSubtitlesAndSelect(trackId); } +extern "C" JNIEXPORT void JNICALL +Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_applyWindowChrome( + JNIEnv *, + jobject, + jlong windowHwnd, + jboolean darkMode, + jint captionColorRgb, + jint borderColorRgb, + jint textColorRgb +) { + applyDwmWindowChrome( + (HWND)(intptr_t)windowHwnd, + darkMode == JNI_TRUE, + rgbIntToColorRef(captionColorRgb), + rgbIntToColorRef(borderColorRgb), + rgbIntToColorRef(textColorRgb) + ); +} + extern "C" JNIEXPORT void JNICALL Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_setSubtitleDelayMs(JNIEnv *, jobject, jlong handle, jint delayMs) { auto player = playerFromHandle(handle);