feat: Implement native desktop window chrome support for Windows

This commit is contained in:
tapframe 2026-06-09 17:19:51 +05:30
parent 128f7322d0
commit 0acce0d3b5
5 changed files with 90 additions and 0 deletions

View file

@ -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 = """

View file

@ -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()

View file

@ -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,
)
}
}

View file

@ -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,

View file

@ -2,6 +2,7 @@
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <dwmapi.h>
#include <wrl.h>
#include <WebView2.h>
#include <jni.h>
@ -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);