From cbe561d30b5a86dfb5dc2d1685b243656a4c278a Mon Sep 17 00:00:00 2001 From: danielz1z Date: Fri, 29 May 2026 16:52:02 +0700 Subject: [PATCH] feat(desktop): dark native Windows title bar Apply DWMWA_USE_IMMERSIVE_DARK_MODE to the desktop window on Windows so the native title bar renders dark (matching Nuvio's near-black UI) instead of the default white caption. Falls back to the pre-20H1 attribute index (19) on older Win10; HRESULT-logged and fully guarded (no-ops on non-Windows / on failure). The window stays decorated, so this is a non-client/caption-only change: native fullscreen (the borderless-fullscreen controller strips the caption anyway), Aero Snap, resize and maximize are all unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../kotlin/com/nuvio/app/DesktopApp.kt | 14 +++++ .../nuvio/app/desktop/DesktopWindowChrome.kt | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 composeApp/src/desktopMain/kotlin/com/nuvio/app/desktop/DesktopWindowChrome.kt diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/DesktopApp.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/DesktopApp.kt index 15a6a038c..9391d6d73 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/DesktopApp.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/DesktopApp.kt @@ -31,6 +31,7 @@ import com.nuvio.app.desktop.DesktopRuntimeLog import com.nuvio.app.desktop.DesktopSingleInstanceManager import com.nuvio.app.desktop.DesktopUriHandler import com.nuvio.app.desktop.DesktopWindowStateStore +import com.nuvio.app.desktop.WindowsChromePolish import com.nuvio.app.desktop.WindowsNativeBootstrap import com.nuvio.app.desktop.WindowsUrlProtocolRegistrar import com.nuvio.app.features.notifications.WindowsToastHelper @@ -56,6 +57,14 @@ private fun configureMacOsNativeAppearance() { System.setProperty("apple.awt.application.appearance", "NSAppearanceNameDarkAqua") } +/** + * Dark native-caption styling is Windows-only (DWM immersive dark mode); macOS/Linux keep their + * native title bars unchanged. The window stays decorated on every platform, so native fullscreen, + * Aero Snap, resize and maximize are untouched. + */ +private val isWindowsOs: Boolean + get() = System.getProperty("os.name")?.contains("Windows", ignoreCase = true) == true + private fun computeStartupWindowSize(): DpSize { val displayBounds = GraphicsEnvironment.getLocalGraphicsEnvironment() .defaultScreenDevice @@ -259,6 +268,11 @@ fun main(args: Array) { window.contentPane.background = DesktopWindowBackground window.rootPane.background = DesktopWindowBackground devStreamMode?.startDiagnostics { desktopMainWindow } + // Recolor the native caption to dark (keeps the window decorated so native + // fullscreen / Aero Snap / resize all keep working). + if (isWindowsOs) { + WindowsChromePolish.apply(window) + } onDispose { desktopMainWindow = null } } diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/desktop/DesktopWindowChrome.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/desktop/DesktopWindowChrome.kt new file mode 100644 index 000000000..b0cf83fee --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/desktop/DesktopWindowChrome.kt @@ -0,0 +1,57 @@ +package com.nuvio.app.desktop + +import androidx.compose.ui.awt.ComposeWindow +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.ptr.IntByReference +import com.sun.jna.win32.StdCallLibrary + +/** + * Darkens the **native** Windows title bar via DWM (immersive dark mode) so it matches Nuvio's + * near-black UI instead of the default white caption. + * + * Deliberately minimal: it sets ONLY the dark-mode caption attribute and keeps the window + * *decorated*. It does NOT set rounded-corner or border-color attributes — those persist into the + * native borderless-fullscreen window state and black out fullscreen video (DWM can no longer do + * direct fullscreen presentation). Window shape is left to the OS default (Win11 rounds decorated + * windows natively). Dark mode is a non-client/caption attribute, so it can't affect the video + * surface. Aero Snap / resize / maximize all keep working because the native frame is untouched. + * + * Best-effort: guarded and HRESULT-logged; no-ops on non-Windows / older OS. + */ +object WindowsChromePolish { + private const val S_OK = 0 + // Dark caption attribute: index 20 on Win10 2004+/Win11, 19 on older Win10 (1809–1909). + private const val DWMWA_USE_IMMERSIVE_DARK_MODE = 20 + private const val DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1 = 19 + + private interface Dwmapi : StdCallLibrary { + fun DwmSetWindowAttribute( + hwnd: Pointer, + dwAttribute: Int, + pvAttribute: IntByReference, + cbAttribute: Int, + ): Int + } + + fun apply(window: ComposeWindow) { + if (System.getProperty("os.name")?.contains("Windows", ignoreCase = true) != true) return + runCatching { + val hwnd = Native.getWindowPointer(window) ?: run { + DesktopRuntimeLog.warn("window chrome polish skipped: null HWND") + return + } + val dwm = Native.load("dwmapi", Dwmapi::class.java) + + // ONLY the dark-caption hint — NOT corner-preference or border-color, which persist into + // the borderless-fullscreen popup window and black out fullscreen video. + var darkHr = dwm.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, IntByReference(1), 4) + if (darkHr != S_OK) { + darkHr = dwm.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1, IntByReference(1), 4) + } + DesktopRuntimeLog.info("window chrome polish hr: darkMode=$darkHr (0=S_OK)") + }.onFailure { + DesktopRuntimeLog.warn("window chrome polish failed ${it::class.simpleName}:${it.message}") + } + } +}