Skip to content

Add Dynamic Alt Tab#4741

Open
chrisc44890 wants to merge 11 commits into
ramensoftware:mainfrom
chrisc44890:main
Open

Add Dynamic Alt Tab#4741
chrisc44890 wants to merge 11 commits into
ramensoftware:mainfrom
chrisc44890:main

Conversation

@chrisc44890

Copy link
Copy Markdown
Contributor

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Changelog item 1...
  • Changelog item 2...

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • [X ] The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • [ X] Other (please specify): Gemini wrote the base code with tweaks and fixed done by a combination of Gemini, Claude, and myself.

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the hooks to twinui_pcshell_dll_hooks in stead of just "hooks"

Attempt to fix Symbols error and hooks warning

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempt #2

Tried to kill two birds with one stone... The stone was unnafective, trying again

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fingers crossed...

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save me Claude

@m417z

m417z commented Jul 11, 2026

Copy link
Copy Markdown
Member

Why does the mod target all processes and not just explorer.exe?

@chrisc44890

Copy link
Copy Markdown
Contributor Author

Why does the mod target all processes and not just explorer.exe?

That's a valid question, I noticed some issues with the interface not showing up with certain programs in the foreground when only explorer.exe is the only thing targeted. The simplest fix was targeting everything.

@m417z

m417z commented Jul 11, 2026

Copy link
Copy Markdown
Member

I prefer to have it understood and solved properly.

@chrisc44890

Copy link
Copy Markdown
Contributor Author

Alright I'll try to figure it out tonight

I think this may have done the trick, also added some checks to prevent the interface from drawing on top of itself.

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should do the trick, also added some checks to make sure the interface doesn't draw on top of itself and to prevent a held tab button from being interpreted as multiple key presses.

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry one last update to fix lag when a game is chosen as the app to open.

@m417z

m417z commented Jul 11, 2026

Copy link
Copy Markdown
Member

What is the trick? Did I miss the answer? (Why does the mod target all processes and not just explorer.exe?)

If the reason is elevated windows, have you tried using RegisterHotKey? It should support elevated windows, and also be better in terms of performance. That's what the Simple Window Switcher mod does, for example.

@chrisc44890

Copy link
Copy Markdown
Contributor Author

Using PostMessageW to call WM_ALTTAB_TRIGGER, I could try RegisterHotKey but from what I understand I don't think it would give a KeyUp event which I'm using to play a closing animation for visual polish.

@chrisc44890

Copy link
Copy Markdown
Contributor Author

Ahhh wait I copy and pasted my readme from before and forgot to change it to just explorer.exe, one sec

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the include line. It works with just explorer.exe now

@m417z

m417z commented Jul 12, 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. No single-instance guard — every explorer.exe spins up its own overlay + global keyboard hook. The mod injects into every explorer.exe, and each instance unconditionally creates a full D2D overlay thread (OverlayThreadProc) and a system-wide WH_KEYBOARD_LL hook (HookThreadProc). Your own comment says "we only want ONE overlay window rendering at a time", but nothing enforces that. On setups with Launch folder windows in a separate process enabled (or any time a second explorer.exe exists), you get multiple overlays and multiple LL hooks all swallowing Alt+Tab and racing to PostMessage whichever window FindWindowW happens to return.

Possible solution: Initialize lazily on first Alt+Tab hook.

2. The LL keyboard hook swallows Alt+Tab unconditionally, even when there's no overlay to show it. In LowLevelKeyboardProc you do if (overlay) PostMessageW(...) but then return 1 regardless. If the overlay window doesn't exist for any reason (render thread failed to create the window, mid-teardown, or the non-shell-explorer case above), Alt+Tab is now globally dead with no replacement — the user can't switch windows at all. Only swallow the key when you actually delivered the trigger:

HWND overlay = FindWindowW(L"GlassAltTabOverlayClass", L"GlassAltTabOverlay");
if (!overlay) return CallNextHookEx(NULL, nCode, wParam, lParam); // let the real Alt+Tab through
PostMessageW(overlay, WM_ALTTAB_TRIGGER, shiftDown ? 1 : 0, 0);
...
return 1;

Be aware this hook also replaces Alt+Tab in every process system-wide (fullscreen games, RDP sessions, etc.), which is inherent to the fallback design — worth calling out in the README.

3. timeBeginPeriod(1) and the 8 ms render timer are held for the mod's entire lifetime, not just while the carousel is up. OverlayThreadProc bumps the system timer resolution to 1 ms and starts an 8 ms (~125 Hz) SetTimer immediately, and both stay active the whole time the mod is loaded. RenderTimerProc early-returns when idle, so CPU is low, but the 1 ms global timer resolution prevents deep CPU C-states and measurably drains battery continuously, even when the user never touches Alt+Tab. A cleaner alternative exists: only call timeBeginPeriod(1) (and, ideally, only run the fast timer) while g_isAltTabbing || g_isClosing, and drop back to idle when the animation finishes.

4. The isWindhawk path and "High Integrity (Windhawk)" handling are dead code given @include explorer.exe. The mod is only ever loaded into explorer.exe, so isWindhawk is always false and the keyboard hook only ever runs at medium integrity (inside Explorer) — which is exactly why it can't see input while an elevated window is focused (as your README correctly notes). The comment "we install the keyboard hook so we can catch keys in Medium Integrity (Explorer) AND High Integrity (Windhawk)" is therefore misleading. Either add @include windhawk.exe if you actually want the high-integrity capture, or remove the isWindhawk branch and fix the comment.

Optional improvements

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

  • Bare-name LoadLibraryW (twinui.pcshell.dll, winmm.dll) uses the default search order, which includes the target .exe's directory. Since the mod is scoped to explorer.exe (always in C:\Windows), the hijack surface is negligible, so this is just hardening: prefer LoadLibraryExW(L"winmm.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32). (twinui.pcshell.dll is already loaded in Explorer, so GetModuleHandleW would usually do — no load needed.)
  • Unused @compilerOptions libs. -lversion and -lshell32 don't appear to be used (GetShellWindow is in user32, not shell32; no version APIs are called). -luuid also looks unnecessary since you only use __uuidof. Remove what isn't needed.
  • g_pDeviceContext is dead. It's QueryInterface'd and released in three places but never used for any drawing (everything goes through g_pDCRenderTarget). Drop the member and the QI.
  • FindWindowW on every keystroke. The LL keyboard hook resolves the overlay via FindWindowW on each Alt/Tab event; since the overlay HWND is stable, cache it (e.g. store g_overlayHwnd's value once) to keep the hook as lean as possible — LL hooks run synchronously on the input path.
  • Log prefixes. Wh_Log(L"Dynamic Alt-Tab: ...") — Windhawk already prefixes log lines with the mod name, so the Dynamic Alt-Tab: prefix is redundant.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Synthetic Alt-up. StopAltTab calls keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0) to un-stick the Alt key. This injects a global key event that any app tracking modifier state will see — a workable hack, but be aware it can confuse apps mid-chord.
  • ShowWindow_Hook blast radius. While the alt-tab state is active you also suppress Shell_InputSwitchTopLevelWindow (the language/IME switcher) and MultitaskingViewFrame (Task View). If those ever overlap with your g_wantsToOpen/g_isAltTabbing window, Win+Tab or the language switcher could be affected. Worth verifying the guard conditions are tight.
  • Per-monitor DPI. The overlay spans the whole virtual screen and draws cards at fixed pixel sizes with a fixed 14 pt font. On high-DPI or mixed-DPI multi-monitor setups the carousel and text won't scale per monitor. Given the single-overlay design there's no trivial fix, but it's a known limitation to document.
  • Full-screen UpdateLayeredWindow at ~125 Hz. Every frame re-blits a bitmap the size of the entire virtual desktop, which is heavy on large/multi-monitor configs even though only a small region changes. Acceptable for the effect, just noting the cost.

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made all the necessary, optional, and functional changes. In this version of the code, including windhawk.exe fixed the elevated process issue without even needing the isWindhawk block so both of those changes were made with regard to the 4th note.

@chrisc44890 chrisc44890 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned elevated process bug with the default alt-tab showing after a logoff or explorer.exe restarting in the README with a temporary fix.

@m417z

m417z commented Jul 12, 2026

Copy link
Copy Markdown
Member

Submission review

Please address the following issues. Also, why is windhawk.exe included now?


1. Crash (Explorer taken down) when the last card is closed — out-of-bounds g_cards[g_selectedIndex]. In the Apple Liquid Glass path (theme 2, the default), the pool computation dereferences the selected card without an empty-vector guard:

// line ~867 and ~882
} else {
    AppCard& activeCard = g_cards[g_selectedIndex];   // OOB if g_cards is empty / index == -1
    ...
}

Reproduction: open the carousel and click the little "✕" close button on the cards one at a time until the last one is gone. Closing the last card runs:

// WM_LBUTTONUP, line ~572
g_cards.erase(g_cards.begin() + i);
if (g_selectedIndex >= (int)g_cards.size()) g_selectedIndex = (int)g_cards.size() - 1;  // -> -1 when empty
if (g_cards.empty()) { g_cancelAltTab = true; g_wantsToOpen = false; }
return 0;

Now g_cards is empty and g_selectedIndex == -1, but g_isAltTabbing is still true and g_introOutroProgress is still ~1.0, so the next RenderTimerProc tick skips the "fully closed" early-out at line ~624 (g_introOutroProgress < 0.02f is false) and falls straight into the g_theme == 2 pool block, hitting g_cards[-1] → UB → crash of explorer.exe. Guard the block (and the g_selectedIndex access generally) against an empty list, e.g. wrap the pool section in if (g_theme == 2 && !g_cards.empty()), and when the last card is removed transition cleanly to the closing state instead of leaving g_isAltTabbing == true with an empty vector.

2. Return-type mismatch on the XamlAltTabViewHost::ViewLoaded hook. The symbol is public: virtual long __cdecl XamlAltTabViewHost::ViewLoaded(void) — it returns long — but the hook and trampoline are typed to return void:

using XamlAltTabViewHost_ViewLoaded_t = void(WINAPI*)(void*);   // should return long
...
void WINAPI XamlAltTabViewHost_ViewLoaded_Hook(void* pThis) {
    AutoThreadId lock;
    XamlAltTabViewHost_ViewLoaded_Original(pThis);              // original's long result is discarded
}

The caller inside the shell expects a long/HRESULT back in rax; your trampoline drops it, so whatever the shell does with that return value gets an undefined value. On x64 it may appear to work (the original's value often survives in rax by luck), but that's fragile. Type both the _t and the hook as long WINAPI ... and return XamlAltTabViewHost_ViewLoaded_Original(pThis);. (DisplayAltTab really is void, so that one is fine.)

Optional improvements

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

  • Settings string read unneeded check. LoadSettings does Wh_GetStringSetting + a NULL check + Wh_FreeStringSetting. Wh_GetStringSetting never returns NULL (it returns L"" when unset), and WindhawkUtils::StringSetting (RAII) removes the manual free:
    WindhawkUtils::StringSetting theme = WindhawkUtils::StringSetting::make(L"theme");
    if (wcscmp(theme, L"3D Glass Carousel") == 0) ...
  • Cross-thread reads of plain bools. ShowWindow_Hook runs on arbitrary Explorer threads and reads g_isAltTabbing / g_isClosing / g_isFirstFrame, which are written on the render thread. It's benign on x86/x64 in practice, but for consistency with the rest of the state (which is std::atomic) consider making these atomic too.
  • Magic constants. DwmSetWindowAttribute(overlayHwnd, 38, ...) (that's DWMWA_SYSTEMBACKDROP_TYPE) and the SetWindowCompositionAttribute attribute = 19 are raw numbers — a named constant or a comment would help the next reader.
  • Dead keyboard path in OverlayWndProc. The low-level hook swallows Alt+Tab (return 1) before it can reach any window, so the overlay's own WM_KEYDOWN/VK_TAB branch never fires; cycling comes exclusively through g_tabSteps. Not harmful, just unreachable.
  • Render thread leaks on a rare init-failure path. In Wh_ModInit, if the keyboard-hook thread fails to start, you return FALSE after the overlay/render thread is already running; since Wh_ModUninit isn't called for a failed init, that thread and its window leak. Tear the render thread down before returning FALSE there.

Functionality notes

Non-critical observations about the feature behavior itself — no concrete change required, just things to be aware of.

  • Multi-monitor layout. Positions are computed against SM_CX/CYVIRTUALSCREEN and centered on the whole virtual desktop, so on a multi-monitor setup the carousel is centered across the union of monitors (and can straddle the bezel gap) rather than appearing on the active/focused monitor. alt-tab-per-monitor is the reference for per-monitor placement if you ever want it.
  • Per-monitor DPI. Card sizes/offsets (380.0f, 300.0f, paddings, etc.) are hard-coded pixels and there's no DPI scaling, so the overlay looks small on a high-DPI display and inconsistent across mixed-DPI monitors. Consider scaling by the target monitor's DPI.
  • ShowWindow suppression scope. The suppression list includes MultitaskingViewFrame (Task View) and Shell_InputSwitchTopLevelWindow (the input/language switcher). They're gated on the alt-tab state, so it's usually fine, but if g_isAltTabbing/g_wantsToOpen is momentarily set when one of those tries to appear, it'd be silently swallowed. Worth keeping in mind if users report Task View / language-switcher hiccups.

@TheGamer1445891

Copy link
Copy Markdown

@Louis047 @Asteski

@Louis047

Copy link
Copy Markdown
Contributor

@TheGamer1445891 @Lockframe thanks for noticing us.

Hmm, interesting mod. Will test it.

Me and @Asteski already made a mod called Simple Window Switcher. May I know what are the novelties in this mod ?

@Louis047

Louis047 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
image

I guess this should be the novelty. But I think it's better if you contribute to our mod as we have already added many features except for the themes and animations 😄

Currently, this mod still needs a lot of work as you can see in the screenshot.
List of issues I have seen:

  • It feels half baked i.e the animations are not consistent and doesn't adapt to the screen refresh rate.
  • Having two or more screens, it displays the switcher in the middle of the two screens which is really bad

I simply believe your best bet should be contibuting to the sws mod where I can add you as a fellow contributor to give credits and such. I'm already working on a draft PR fixing a few bugs and minor feature improvements here: #4776

@chrisc44890

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.

4 participants