Skip to content
Open
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
202 changes: 165 additions & 37 deletions mods/auto-custom-titlebar-colors.wh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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==

Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -111,6 +108,9 @@ Custom colours are only applied when the corresponding "Use Custom Colours" togg

#include <windows.h>
#include <dwmapi.h>
#include <unordered_set>
#include <unordered_map>
#include <mutex>

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
Expand All @@ -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<HWND> g_appControlledWindows;
static std::mutex g_appControlledMutex;
thread_local bool g_inMod = false;

static std::mutex g_settingsMutex;

static std::unordered_map<HWND, BOOL> 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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<std::mutex> 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<std::mutex> lock(g_eligibilityMutex);
g_eligibilityCache[hWnd] = isEligible;
}

return isEligible;
}

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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",
Expand All @@ -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.
Expand Down Expand Up @@ -346,6 +454,18 @@ static VOID HandleWindowMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara
{
switch (Msg)
{
case WM_NCDESTROY:
{
{
std::lock_guard<std::mutex> lock(g_appControlledMutex);
g_appControlledWindows.erase(hWnd);
}
{
std::lock_guard<std::mutex> lock(g_eligibilityMutex);
g_eligibilityCache.erase(hWnd);
}
break;
}
case WM_ACTIVATE:
{
BOOL isActive = (LOWORD(wParam) != WA_INACTIVE);
Expand Down Expand Up @@ -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");

Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -546,6 +669,11 @@ static BOOL CALLBACK UninitEnumWindowsProc(HWND hWnd, LPARAM)
return TRUE;
if (!IsWindowEligible(hWnd)) return TRUE;

{
std::lock_guard<std::mutex> lock(g_appControlledMutex);
if (g_appControlledWindows.count(hWnd)) return TRUE;
}

BOOL off = FALSE;
DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &off, sizeof(off));

Expand Down
Loading