diff --git a/mods/auto-custom-titlebar-colors.wh.cpp b/mods/auto-custom-titlebar-colors.wh.cpp index 2f602e2b38..790e8ca5f4 100644 --- a/mods/auto-custom-titlebar-colors.wh.cpp +++ b/mods/auto-custom-titlebar-colors.wh.cpp @@ -2,13 +2,11 @@ // @id auto-custom-titlebar-colors // @name Auto Custom Titlebar Colors // @description Auto-switches titlebar dark/light mode with the Windows theme, with separate custom colours for active/inactive windows in both modes -// @version 1.1.2 +// @version 1.2.0 // @author Lone // @github https://github.com/Louis047 // @include * // @exclude devenv.exe -// @exclude systemsettings.exe -// @exclude applicationframehost.exe // @compilerOptions -ldwmapi -luxtheme -luser32 // ==/WindhawkMod== @@ -40,7 +38,6 @@ Custom colours are only applied when the corresponding "Use Custom Colours" togg ## Notes - `systemsettings.exe` and `applicationframehost.exe` are excluded to avoid conflicts -- No forced repaint is issued while a mouse button is held (prevents drag-state corruption) - Window redraws are debounced (50ms minimum) to prevent interference with window managers - Visual redraws (SetWindowPos) only occur during window activation, not deactivation, to avoid focus-grabbing issues with tiling window managers */ @@ -111,6 +108,9 @@ Custom colours are only applied when the corresponding "Use Custom Colours" togg #include #include +#include +#include +#include #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 @@ -124,6 +124,16 @@ typedef HRESULT(WINAPI* pShouldSystemUseDarkMode)(); static pShouldSystemUseDarkMode g_ShouldSystemUseDarkMode = nullptr; static BOOL g_isDarkMode = FALSE; +// App-controlled window state tracking +static std::unordered_set g_appControlledWindows; +static std::mutex g_appControlledMutex; +thread_local bool g_inMod = false; + +static std::mutex g_settingsMutex; + +static std::unordered_map g_eligibilityCache; +static std::mutex g_eligibilityMutex; + // Debounce mechanism: track last SetWindowPos time per window to prevent // rapid successive redraws that can interfere with window managers static DWORD g_lastSetWindowPosTime = 0; @@ -167,6 +177,24 @@ BOOL IsSystemDarkMode() // Window eligibility // ----------------------------------------------------------------------------- +struct XamlSearchContext { + BOOL hasXaml; +}; + +static BOOL CALLBACK CheckXamlChildProc(HWND hwnd, LPARAM lParam) { + WCHAR className[256]; + if (GetClassNameW(hwnd, className, 256)) { + if (wcscmp(className, L"DesktopWindowXamlSource") == 0 || + wcscmp(className, L"Windows.UI.Core.CoreWindow") == 0 || + wcscmp(className, L"Microsoft.UI.Content.DesktopChildSiteBridge") == 0 || + wcscmp(className, L"MozillaCompositorWindowClass") == 0) { + ((XamlSearchContext*)lParam)->hasXaml = TRUE; + return FALSE; // Stop enumerating, we found it! + } + } + return TRUE; // Continue enumerating +} + BOOL IsWindowEligible(HWND hWnd) { if (!hWnd || !IsWindow(hWnd)) return FALSE; @@ -178,7 +206,33 @@ BOOL IsWindowEligible(HWND hWnd) if (styleEx & WS_EX_TOOLWINDOW) return FALSE; if (style & WS_CHILD) return FALSE; - return TRUE; + // Fast top-level check for Mozilla/Gecko browsers (Firefox, Zen, Floorp) + WCHAR className[256]; + if (GetClassNameW(hWnd, className, 256)) { + if (wcscmp(className, L"MozillaWindowClass") == 0) { + return FALSE; + } + } + + { + std::lock_guard lock(g_eligibilityMutex); + auto it = g_eligibilityCache.find(hWnd); + if (it != g_eligibilityCache.end()) return it->second; + } + + // Detect WinUI 3 / UWP modern apps and complex composite windows + // These apps host their modern UI inside specific child bridge windows. + XamlSearchContext ctx = { FALSE }; + EnumChildWindows(hWnd, CheckXamlChildProc, (LPARAM)&ctx); + + BOOL isEligible = !ctx.hasXaml; + + { + std::lock_guard lock(g_eligibilityMutex); + g_eligibilityCache[hWnd] = isEligible; + } + + return isEligible; } // ----------------------------------------------------------------------------- @@ -217,59 +271,111 @@ static BOOL HexToColorref(PCWSTR hex, COLORREF* out) // Settings helpers // ----------------------------------------------------------------------------- -static BOOL UseCustomColourForMode(BOOL isDarkMode) -{ - return (BOOL)Wh_GetIntSetting(isDarkMode - ? L"customColours.dark" - : L"customColours.light"); -} - -static COLORREF GetTitleBarColour(BOOL isDarkMode, BOOL isActive) +struct ModSettings { + BOOL useCustomLight; + BOOL useCustomDark; + COLORREF activeLight; + COLORREF inactiveLight; + COLORREF activeDark; + COLORREF inactiveDark; +}; +static ModSettings g_settings; + +static void LoadSettings() { - const WCHAR* modePrefix = isDarkMode ? L"darkMode" : L"lightMode"; - const WCHAR* statePrefix = isActive ? L"activeColour" : L"inactiveColour"; + std::lock_guard lock(g_settingsMutex); - WCHAR useHexKey[64], hexKey[64], rKey[64], gKey[64], bKey[64]; - wsprintfW(useHexKey, L"%s.useHex", modePrefix); - wsprintfW(hexKey, L"%s.%s.hex", modePrefix, statePrefix); - wsprintfW(rKey, L"%s.%s.r", modePrefix, statePrefix); - wsprintfW(gKey, L"%s.%s.g", modePrefix, statePrefix); - wsprintfW(bKey, L"%s.%s.b", modePrefix, statePrefix); + g_settings.useCustomLight = (BOOL)Wh_GetIntSetting(L"customColours.light"); + g_settings.useCustomDark = (BOOL)Wh_GetIntSetting(L"customColours.dark"); - BOOL useHex = (BOOL)Wh_GetIntSetting(useHexKey); + BOOL useHexLight = (BOOL)Wh_GetIntSetting(L"lightMode.useHex"); + if (useHexLight) { + PCWSTR hexActive = Wh_GetStringSetting(L"lightMode.activeColour.hex"); + if (!HexToColorref(hexActive, &g_settings.activeLight)) g_settings.activeLight = RGB(255, 255, 255); + Wh_FreeStringSetting(hexActive); - if (useHex) { - PCWSTR hexVal = Wh_GetStringSetting(hexKey); - COLORREF colour = 0; - BOOL ok = HexToColorref(hexVal, &colour); - Wh_FreeStringSetting(hexVal); + PCWSTR hexInactive = Wh_GetStringSetting(L"lightMode.inactiveColour.hex"); + if (!HexToColorref(hexInactive, &g_settings.inactiveLight)) g_settings.inactiveLight = RGB(230, 230, 230); + Wh_FreeStringSetting(hexInactive); + } else { + g_settings.activeLight = RGB( + (BYTE)Wh_GetIntSetting(L"lightMode.activeColour.r"), + (BYTE)Wh_GetIntSetting(L"lightMode.activeColour.g"), + (BYTE)Wh_GetIntSetting(L"lightMode.activeColour.b") + ); + g_settings.inactiveLight = RGB( + (BYTE)Wh_GetIntSetting(L"lightMode.inactiveColour.r"), + (BYTE)Wh_GetIntSetting(L"lightMode.inactiveColour.g"), + (BYTE)Wh_GetIntSetting(L"lightMode.inactiveColour.b") + ); + } - if (ok) return colour; + BOOL useHexDark = (BOOL)Wh_GetIntSetting(L"darkMode.useHex"); + if (useHexDark) { + PCWSTR hexActive = Wh_GetStringSetting(L"darkMode.activeColour.hex"); + if (!HexToColorref(hexActive, &g_settings.activeDark)) g_settings.activeDark = RGB(32, 32, 32); + Wh_FreeStringSetting(hexActive); - // Invalid hex: log and fall through to RGB - Wh_Log(L"GetTitleBarColour: invalid hex for key '%s', falling back to RGB", hexKey); + PCWSTR hexInactive = Wh_GetStringSetting(L"darkMode.inactiveColour.hex"); + if (!HexToColorref(hexInactive, &g_settings.inactiveDark)) g_settings.inactiveDark = RGB(50, 50, 50); + Wh_FreeStringSetting(hexInactive); + } else { + g_settings.activeDark = RGB( + (BYTE)Wh_GetIntSetting(L"darkMode.activeColour.r"), + (BYTE)Wh_GetIntSetting(L"darkMode.activeColour.g"), + (BYTE)Wh_GetIntSetting(L"darkMode.activeColour.b") + ); + g_settings.inactiveDark = RGB( + (BYTE)Wh_GetIntSetting(L"darkMode.inactiveColour.r"), + (BYTE)Wh_GetIntSetting(L"darkMode.inactiveColour.g"), + (BYTE)Wh_GetIntSetting(L"darkMode.inactiveColour.b") + ); } - - BYTE r = (BYTE)Wh_GetIntSetting(rKey); - BYTE g = (BYTE)Wh_GetIntSetting(gKey); - BYTE b = (BYTE)Wh_GetIntSetting(bKey); - return RGB(r, g, b); } // ----------------------------------------------------------------------------- // Core: apply dark-mode attribute + caption colour to one window // ----------------------------------------------------------------------------- +using DwmSetWindowAttribute_t = decltype(&DwmSetWindowAttribute); +static DwmSetWindowAttribute_t DwmSetWindowAttribute_orig; + +HRESULT WINAPI DwmSetWindowAttribute_hook(HWND hwnd, DWORD dwAttribute, LPCVOID pvAttribute, DWORD cbAttribute) +{ + if (!g_inMod && dwAttribute == DWMWA_CAPTION_COLOR) { + std::lock_guard lock(g_appControlledMutex); + g_appControlledWindows.insert(hwnd); + Wh_Log(L"App controls DWMWA_CAPTION_COLOR for hWnd=%p", hwnd); + } + return DwmSetWindowAttribute_orig(hwnd, dwAttribute, pvAttribute, cbAttribute); +} + static VOID ApplyTitleBar(HWND hWnd, BOOL isActive, BOOL forceRedraw) { if (!IsWindowEligible(hWnd)) return; + { + std::lock_guard lock(g_appControlledMutex); + if (g_appControlledWindows.count(hWnd)) return; + } + + g_inMod = true; BOOL darkMode = g_isDarkMode; + DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkMode, sizeof(darkMode)); - if (UseCustomColourForMode(g_isDarkMode)) { - COLORREF colour = GetTitleBarColour(g_isDarkMode, isActive); + BOOL useCustom; + COLORREF colour; + { + std::lock_guard lock(g_settingsMutex); + useCustom = g_isDarkMode ? g_settings.useCustomDark : g_settings.useCustomLight; + colour = g_isDarkMode + ? (isActive ? g_settings.activeDark : g_settings.inactiveDark) + : (isActive ? g_settings.activeLight : g_settings.inactiveLight); + } + + if (useCustom) { DwmSetWindowAttribute(hWnd, DWMWA_CAPTION_COLOR, &colour, sizeof(colour)); Wh_Log(L"ApplyTitleBar: hWnd=%p dark=%d active=%d colour=#%06X", @@ -280,6 +386,8 @@ static VOID ApplyTitleBar(HWND hWnd, BOOL isActive, BOOL forceRedraw) Wh_Log(L"ApplyTitleBar: hWnd=%p dark=%d active=%d colour=DEFAULT", hWnd, g_isDarkMode, isActive); } + + g_inMod = false; // Debounce SetWindowPos calls to prevent interference with window managers. // Only allow redraws if enough time has passed since the last redraw. @@ -346,6 +454,18 @@ static VOID HandleWindowMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara { switch (Msg) { + case WM_NCDESTROY: + { + { + std::lock_guard lock(g_appControlledMutex); + g_appControlledWindows.erase(hWnd); + } + { + std::lock_guard lock(g_eligibilityMutex); + g_eligibilityCache.erase(hWnd); + } + break; + } case WM_ACTIVATE: { BOOL isActive = (LOWORD(wParam) != WA_INACTIVE); @@ -500,6 +620,7 @@ BOOL Wh_ModInit() Wh_Log(L"=== Auto Custom Titlebar Colors - Init [PID %d] ===", GetCurrentProcessId()); + LoadSettings(); g_isDarkMode = IsSystemDarkMode(); Wh_Log(L"Initial theme: %s", g_isDarkMode ? L"DARK" : L"LIGHT"); @@ -516,6 +637,7 @@ BOOL Wh_ModInit() hook((void*)DefDlgProcA, (void*)DefDlgProcA_hook, (void**)&DefDlgProcA_orig, L"DefDlgProcA"); hook((void*)CreateWindowExW, (void*)CreateWindowExW_hook, (void**)&CreateWindowExW_orig, L"CreateWindowExW"); hook((void*)CreateWindowExA, (void*)CreateWindowExA_hook, (void**)&CreateWindowExA_orig, L"CreateWindowExA"); + hook((void*)DwmSetWindowAttribute, (void*)DwmSetWindowAttribute_hook, (void**)&DwmSetWindowAttribute_orig, L"DwmSetWindowAttribute"); Wh_Log(L"=== Init complete ==="); return TRUE; @@ -533,6 +655,7 @@ VOID Wh_ModAfterInit() VOID Wh_ModSettingsChanged() { Wh_Log(L"[PID %d] Settings changed - reapplying...", GetCurrentProcessId()); + LoadSettings(); g_isDarkMode = IsSystemDarkMode(); ApplyToForegroundWindow(); ApplyToAllWindows(FALSE); @@ -546,6 +669,11 @@ static BOOL CALLBACK UninitEnumWindowsProc(HWND hWnd, LPARAM) return TRUE; if (!IsWindowEligible(hWnd)) return TRUE; + { + std::lock_guard lock(g_appControlledMutex); + if (g_appControlledWindows.count(hWnd)) return TRUE; + } + BOOL off = FALSE; DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &off, sizeof(off));