Skip to content

[Auto Custom Titlebar Colors]: Smart ignore UWP Apps and browser windows with settings caching#4797

Open
Louis047 wants to merge 4 commits into
ramensoftware:mainfrom
Louis047:act-update
Open

[Auto Custom Titlebar Colors]: Smart ignore UWP Apps and browser windows with settings caching#4797
Louis047 wants to merge 4 commits into
ramensoftware:mainfrom
Louis047:act-update

Conversation

@Louis047

@Louis047 Louis047 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Changelog

  • Added a new method to detect and ignore UWP apps by default
  • Added classes to ignore browser windows (conflict with Zen Browser which is a special case)

Issue References

@Louis047

Copy link
Copy Markdown
Contributor Author

Keeping this as a draft until the user confirm it works.

@Louis047 Louis047 changed the title [Auto Custom Titlebar Colors]: Fix UWPSpy conflict issue and caching settings [Auto Custom Titlebar Colors]: Smart ignore UWP Apps and browser windows with settings caching Jul 15, 2026
@Louis047
Louis047 marked this pull request as ready for review July 19, 2026 06:56
@Louis047

Copy link
Copy Markdown
Contributor Author

The user has confirmed that it works, but with a very minor caveat where they have to wait a second or two to let it work normally

@m417z

m417z commented Jul 20, 2026

Copy link
Copy Markdown
Member

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


1. Wh_ModUninit wipes the caption color of app-controlled windows. The whole point of the new g_appControlledWindows set is to never touch windows whose owning app set DWMWA_CAPTION_COLOR itself. But UninitEnumWindowsProc doesn't consult that set — it resets DWMWA_CAPTION_COLOR to DWMWA_COLOR_DEFAULT (and clears immersive dark mode) on every eligible window:

// UninitEnumWindowsProc — mods/auto-custom-titlebar-colors.wh.cpp:663
if (!IsWindowEligible(hWnd)) return TRUE;
BOOL off = FALSE;
DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &off, sizeof(off));
const COLORREF def = DWMWA_COLOR_DEFAULT;
DwmSetWindowAttribute(hWnd, DWMWA_CAPTION_COLOR, &def, sizeof(def));

So when the mod is disabled/reloaded, any window the app itself colored gets its color blown away and reset to the system default — the opposite of "leave it alone". Since many apps set their caption color only once (at window creation), it won't come back until the app restarts. Skip windows in g_appControlledWindows here (and ideally only reset windows the mod actually colored). This also makes the unload non-reversible for those windows, which is a core Windhawk principle.

2. g_appControlledWindows / seenWindows grow forever and key on recyclable HWND values. Neither set ever removes an entry when a window is destroyed:

static std::unordered_set<HWND> g_appControlledWindows;   // never erased
...
static std::unordered_set<HWND> seenWindows;              // never erased

Two consequences in a long-lived, broadly-injected (@include *) process like explorer.exe:

  • Stale-HWND false positives: Windows recycles HWND values. Once a tracked window is destroyed, a brand-new, unrelated window can be created with the same handle value — it will be found in g_appControlledWindows and silently never colored (item stays until the process dies).
  • Unbounded growth: both sets accumulate one entry per window ever seen, for the life of the process.

You already hook DefWindowProcW/A, so HandleWindowMessage can handle WM_NCDESTROY and erase(hWnd) from both sets to bound the lifetime and fix the reuse hazard.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • The two branches in ApplyTitleBar are near-identical. After Wh_ModInit succeeds, DwmSetWindowAttribute_orig is always non-null, so the else branch (lines 380–399) that calls the hooked DwmSetWindowAttribute directly is effectively dead and duplicates the whole color-selection block. Since g_inMod already guards the hook from re-tracking the mod's own calls, you can drop the branch and just call DwmSetWindowAttribute (or _orig) once — the g_inMod flag makes either safe.

  • Stale README note. The ## Notes section still says "systemsettings.exe and applicationframehost.exe are excluded to avoid conflicts" (line 40), but this PR removed those @exclude lines in favor of runtime detection. Update or drop that line.

  • g_settings is written on the settings-change thread and read on window threads without synchronization. It's a POD struct, so the worst case is one repaint using a half-updated struct (e.g. useCustom toggled but a color not yet stored) right after a settings save — no crash, self-corrects on the next paint. Only worth a mention since the pre-cache code read each value atomically per call.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The DwmGetWindowAttribute "already-colored" fallback almost certainly never fires. DWMWA_CAPTION_COLOR is a set-only attribute — DwmGetWindowAttribute returns E_INVALIDARG for it on current Windows, so SUCCEEDED(...) is false and the inner block (and its Wh_Log) never runs:

    // ApplyTitleBar — mods/auto-custom-titlebar-colors.wh.cpp:349
    if (SUCCEEDED(DwmGetWindowAttribute(hWnd, DWMWA_CAPTION_COLOR, &currentColor, sizeof(currentColor)))) {
        if (currentColor != DWMWA_COLOR_DEFAULT) { ... }   // unreachable
    }

    That means an app which set its own caption color before the mod loaded (mid-session enable) isn't detected via this path — only the DwmSetWindowAttribute hook (which catches sets after the hook is installed) works. Easy to confirm: enable logging and check whether the "already has custom … ignoring" line ever appears. If it doesn't, the seenWindows set + this block are pure overhead and can be removed.

  • EnumChildWindows runs on every ApplyTitleBar call, in every process. Each eligibility check that doesn't short-circuit on MozillaWindowClass walks the full child-window tree calling GetClassNameW per descendant, and ApplyTitleBar fires on WM_NCACTIVATE, WM_ACTIVATE, CreateWindowEx, etc. across @include *. For most windows the tree is small so it's fine, but for complex apps this repeats a full subtree walk on every activation. If it shows up in practice, consider caching a per-HWND eligibility verdict (invalidated on WM_NCDESTROY, alongside item 2 above).

  • The DesktopWindowXamlSource check is broad. A growing number of otherwise-normal Win32 apps host XAML islands (DesktopWindowXamlSource) for parts of their UI while still using a standard caption. Those will now be skipped entirely. That's likely the intended trade-off for the crash fix, but worth being aware that the excluded set is wider than "UWP apps" — it's "anything hosting a XAML island / CoreWindow".

@Louis047

Copy link
Copy Markdown
Contributor Author

Noted, will work on this when I have the time.

@Louis047

Copy link
Copy Markdown
Contributor Author

@m417z Hope this settles it

@m417z

m417z commented Jul 22, 2026

Copy link
Copy Markdown
Member

Submission review

There are additional notes regarding the mod functionality. I didn't verify them, but they might be worth fixing. Take a look, if you think the mod is ready as is, let me know.


1. The eligibility cache is poisoned at window creation — the structural UWP/WinUI detection never fires for windows created after the mod loads. CreateWindowExW/A_hook calls ApplyTitleBar immediately after the top-level window is created (line 594), at which point the window has no children yet. IsWindowEligible runs EnumChildWindows, finds no CoreWindow/DesktopWindowXamlSource/DesktopChildSiteBridge, and caches isEligible = TRUE (lines 225–233) — an entry that's only evicted at WM_NCDESTROY. When the bridge child appears moments later, every subsequent check (WM_NCACTIVATE, theme changes, …) hits the stale TRUE and the mod colors the window anyway. Net effect: the detection only works for windows that already exist at Wh_ModAfterInit time; any UWP/WinUI 3 window opened afterwards is treated as a normal window — reintroducing exactly the conflict from issue 4582. (The create-time ApplyTitleBar also colors the window once before detection could possibly succeed, which likely explains the "wait a second or two" caveat the reporter mentioned.)

Suggested fix, in two parts:

  • Don't populate the cache from the CreateWindowEx path — pass a "don't cache" flag (or skip the ApplyTitleBar eligibility caching there), so the first activation-time check, which runs when the children exist, is the one that gets cached.
  • Additionally, invalidate on child creation: in the CreateWindowEx hooks, GetClassNameW the new window, and if it's one of the bridge classes, mark GetAncestor(hWnd, GA_ROOT) ineligible in the cache — and reset that root's DWMWA_CAPTION_COLOR to DWMWA_COLOR_DEFAULT, since the create-time apply may already have colored it. Note this doesn't cover the UWP ApplicationFrameHost case, where the CoreWindow is created in the app's process and reparented into the frame window cross-process — so the first bullet (defer caching to activation time) is the important one; the child-creation invalidation covers same-process XAML/WinUI 3 windows.

2. Dialogs never reach HandleWindowMessage, so their WM_NCDESTROY cleanup never runs. The DefDlgProcW/A hooks exist precisely because dialogs don't route through the exported DefWindowProcW/A — but they only special-case WM_NCACTIVATE (lines 553–576). For every dialog, the g_eligibilityCache / g_appControlledWindows entries are therefore never erased, which brings back the two hazards from the previous review round for this window class: unbounded growth, and stale entries on recycled HWND values (a new unrelated window reusing a destroyed dialog's handle inherits its cached eligibility / app-controlled status). Dialogs also miss the WM_ACTIVATE / WM_SETTINGCHANGE / WM_DWMCOLORIZATIONCOLORCHANGED handling. Fix: call HandleWindowMessage(hWnd, Msg, wParam, lParam) from both DefDlgProc hooks and drop the duplicated WM_NCACTIVATE block (double-handling in case both hooks fire for a message is harmless — the erase is idempotent and ApplyTitleBar is repeat-safe).

3. Wh_ModUninit still force-resets DWMWA_USE_IMMERSIVE_DARK_MODE to FALSE on windows whose app set it itself. g_appControlledWindows only tracks DWMWA_CAPTION_COLOR, but many modern Win32 apps (terminals, editors, Chromium/Electron apps) opt into dark titlebars themselves via this same attribute, typically once at window creation. Disabling the mod while the system is in dark mode flips those titlebars to light until the app restarts (lines 677–678) — the same reversibility problem as the caption-color half you already fixed. Suggested fix: in DwmSetWindowAttribute_hook, also track app-initiated sets of DWMWA_USE_IMMERSIVE_DARK_MODE (e.g. an HWND → BOOL map of the app's last value, cleaned up at WM_NCDESTROY like the others), and at uninit restore that recorded value instead of unconditionally writing FALSE.

Optional improvements

Minor polish — none of this affects users much, so it's your call.

  • Stale README note (carried over from the previous review). The ## Notes section still says "systemsettings.exe and applicationframehost.exe are excluded to avoid conflicts" (line 40), but those @exclude lines are gone. While updating it, consider documenting the new behavior instead: UWP/WinUI windows and Mozilla-based browsers are now auto-detected and skipped.
  • -luxtheme is unused. uxtheme.dll is only accessed via GetModuleHandleW + GetProcAddress; nothing links against it, so the @compilerOptions flag can be dropped.
  • The debounce comment doesn't match the code. The comment says "track last SetWindowPos time per window", but g_lastSetWindowPosTime is a single global shared by all windows (and written from multiple threads without synchronization) — two windows activating within 50 ms means the second silently skips its frame refresh. Either make it per-window or fix the comment; making the variable std::atomic<DWORD> would also address the benign data race.
  • g_isDarkMode is written from arbitrary window threads (WM_SETTINGCHANGE handling) and read everywhere; std::atomic<BOOL> would make that clean.
  • EnumWindowsProc can be simplified: the if/else reduces to ApplyTitleBar(hWnd, isActive, forceRedraw && isActive), and the GetAncestor(GA_PARENT) check is redundant — EnumWindows only enumerates top-level windows.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The GetAsyncKeyState(VK_LBUTTON) gate suppresses the refresh for the most common activation path. When a user clicks a window to activate it, the button is still down when WM_ACTIVATE/WM_NCACTIVATE arrive, so the SetWindowPos refresh essentially only ever runs for keyboard activation (Alt-Tab). In practice DWMWA_CAPTION_COLOR changes take effect without SWP_FRAMECHANGED, so this mostly doesn't show — which raises the question of whether the SetWindowPos (and the debounce machinery around it) is needed at all. Worth testing without it. Also, the README note documenting the mouse-button suppression was removed in this PR while the code behavior remains.
  • MozillaWindowClass excludes every Gecko browser, not just Zen. Firefox and Floorp users who previously had titlebar coloring working will silently lose it with this update. If the conflict is specific to Zen's custom styling, consider making the browser exclusion a setting (default on is fine) so users can opt back in. Relatedly, MozillaCompositorWindowClass in the child scan is redundant — a compositor child only exists under a MozillaWindowClass top-level, which the fast check already rejects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants