Skip to content

Add Taskbar Blob Shape mod - #4941

Merged
m417z merged 16 commits into
ramensoftware:mainfrom
Deen-0x:add-taskbar-blob-shape-mod
Aug 3, 2026
Merged

Add Taskbar Blob Shape mod#4941
m417z merged 16 commits into
ramensoftware:mainfrom
Deen-0x:add-taskbar-blob-shape-mod

Conversation

@Deen-0x

@Deen-0x Deen-0x commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Taskbar Blob Shape

Add Taskbar Blob Shape mod which replaces the rounded-rectangle indicator behind Windows 11 taskbar buttons with a parametric "blob" — a tab-like shape whose top edge is flat and wide, with concave flares at the top corners so each button reads as a tab merging into the desktop above it.

Attribution

Use case

blob lhSj7xL9B2

Features

  • Parametric shape — top corner radius (the concave flares; also sets how far the shape extends up and outward), bottom corner radius, custom width/height (auto matches the button's background element), and margin offsets.
  • Color — custom hex fill with multi-color gradient support and separate light | dark values, per-theme opacity multipliers, and automatic fallback to the system accent color when no custom color is set.
  • Works across multiple monitors and taskbars.

Implementation notes

  • Hooks TaskListButton::UpdateVisualStates (Taskbar.View.dll) to observe running indicator state changes; activation is a pure per-button opacity toggle.
  • Each button gets its own Path element, hosted in the taskbar's RootGrid (above the task list's clipping region so the flare tips render fully) and glued to its button with a one-time composition ExpressionAnimation on the button's visual offset chain — the shape tracks reordering, reflow, and taskbar animations on the render thread with no per-frame UI-thread work.
  • Full button lifecycle tracking: an Unloaded handler per button hides its shape and invalidates the binding when the button leaves the tree (window moved to another monitor, app closed, container recycled), and reused containers re-resolve their hosting grid, re-parent the shape if needed, and rebind — shapes never outlive or detach from their buttons.
  • The native BackgroundElement is suppressed via its hand-off visual's IsVisible flag rather than a local Opacity value, so visual-state transition storyboards (which outrank local values in XAML property precedence) can't bring it back on top of the shape.
  • Late layout is handled with a SizeChanged subscription per button — no timers, no polling.
  • Unloading detaches all event handlers, stops the composition animations, removes the injected elements, and restores the native indicators.

Tested on

  • Windows 11 25H2 (OS Build 26200.8894), single and dual monitor
  • Reordering pinned items, launching pinned apps, minimize/restore, dragging windows between monitors in both directions (including button removal and container reuse), overflow-free and mixed light/dark themes, enable/disable cycles

Mod authorship

This mod was created by: Claude

Deen-0x added 2 commits July 29, 2026 08:50
Add Taskbar Blob Shape mod which replaces the rounded-rectangle indicator behind Windows 11 taskbar buttons with a parametric "blob" — a tab-like shape whose top edge is flat and wide, with concave flares at the top corners so each button reads as a tab merging into the desktop above it.
… its own BackgroundElement | Cleanup restores both: IsVisible(true)

Suppression moved into EnsureBlobOnButton and switched to GetElementVisual(bg).IsVisible(!isActive). Since every path — the hook, SizeChanged, settings changes — funnels through this function, the flag is re-asserted consistently no matter which event fired, and no storyboard the visual state manager plays can override it.

The hook callback dropped its own BackgroundElement opacity block — one mechanism, one place.

Cleanup restores both: IsVisible(true) on the visual, plus Opacity(1.0) as a belt-and-suspenders restore in case a local zero from an earlier build of the mod is still sitting on a live element when you update without restarting explorer.
@Deen-0x
Deen-0x marked this pull request as ready for review July 29, 2026 06:45
@Deen-0x
Deen-0x marked this pull request as draft July 29, 2026 07:18
Unloaded handler per button (attached once, tracked via unloadAttached + token in the entry): when the button leaves the tree, its blob is hidden immediately and the entry's grid cache and expression binding are invalidated. Since the opacity gate is isActive && bound, the blob physically cannot reappear until a fresh, valid binding exists.

Re-parent check for the reuse path: if a recycled container resurfaces under a different taskbar's RootGrid (containers hopping monitors), the existing blob is moved from the old grid into the new one — below its repeater, same as at creation — and forced to rebind against the new chain. The parentGrid != grid comparison also covers the degenerate case of a blob that somehow lost its parent entirely.

Cleanup detaches the Unloaded subscription alongside the existing SizeChanged teardown.

The Unloaded → invalidate → next-event → re-resolve flow reuses machinery that already existed (GetTaskbarRootGrid on a null grid cache, BindBlobExpression on bound == false), so the steady-state hot path didn't change at all.
@Deen-0x
Deen-0x marked this pull request as ready for review July 29, 2026 09:02
@Deen-0x

Deen-0x commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-ai-review An AI review was requested and is being prepared. label Jul 30, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


The overall design (one Path per button hosted in the taskbar RootGrid, glued to its button with a composition expression) is sound, but a few things need fixing before this ships into explorer.exe.

1. Remove the VEH / QueryInterface-probing machinery used to get the XAML element (IsSafeComPointer, SafeComCallVEH, SafeProbeHelper, ProbeForFrameworkElement).

This is the most serious item. ProbeForFrameworkElement brute-forces offsets 1..6 inside the native TaskListButton and calls QueryInterface on whatever it finds, "protected" by a vectored exception handler that jumps out of the fault:

  • A pointer that merely looks like it has a vtable is not necessarily a COM object — calling slot 0 of a non-COM vtable invokes an arbitrary virtual function with QI's arguments. A VEH cannot protect against that; it corrupts the shell rather than faulting.
  • When it does fault, RtlRestoreContext is a non-unwinding jump out of the middle of XAML's QueryInterface. Any lock or internal state those abandoned frames held stays held, producing a deadlock or corruption in Explorer later, far from the cause.
  • AddVectoredExceptionHandler/RemoveVectoredExceptionHandler runs up to 6 times per UpdateVisualStates call, and while installed the handler is process-wide: it sees every exception on every Explorer thread.
  • IsSafeComPointer's VirtualQuery validation is the same discredited pattern as IsBadReadPtr (https://devblogs.microsoft.com/oldnewthing/20060927-07/?p=29563) — it is a TOCTOU check, and it proves nothing about the pointer being a COM object.

The mod this is derived from already does this in six lines — taskbar-elastic-pill.wh.cpp#L734-L743, same as taskbar-labels.wh.cpp#L1346-L1350:

FrameworkElement GetFrameworkElementFromNative(void* pThis) {
    if (!pThis) return nullptr;
    try {
        void* iUnknownPtr = (void**)pThis + 3;
        winrt::Windows::Foundation::IUnknown iUnknown;
        winrt::copy_from_abi(iUnknown, iUnknownPtr);
        return iUnknown.try_as<FrameworkElement>();
    } catch (...) {
        return nullptr;
    }
}

Dropping the probe machinery removes ~80 lines and all of the above risk. (If it was an AI-generated "make it robust" addition rather than a deliberate choice, that's another reason to remove it.)

2. LoadLibraryExW_Hook defers hooking to a detached thread with Sleep(2000) and a global named mutex.

Four separate problems in that block:

  • The thread is detached and nobody waits for it. If the mod is disabled, updated or reloaded during those 2 seconds, the thread wakes up in an unmapped DLL — and calls HookTaskbarViewDllSymbols / Wh_ApplyHookOperations after Wh_ModUninit. That's an Explorer crash.
  • WaitForSingleObject(hMutex, INFINITE) on the Global\WindhawkElasticModsHookMutex named mutex can block that thread indefinitely on state owned by another process, and nothing else in this mod (or in the merged elastic pill) uses that mutex — it serializes nothing.
  • The 2 s delay means buttons that appear during Explorer startup get no blob until their next visual-state change.
  • The deferred path only accepts Taskbar.View.dll (GetModuleHandleExW(0, L"Taskbar.View.dll", ...)), while GetTaskbarViewModuleHandle() also matches ExplorerExtensions.dll — on such a build the hooks are never applied at all and the mod silently does nothing.

Hook inline in the loader hook, like the mod you derived from (taskbar-elastic-pill.wh.cpp#L1889-L1902):

HMODULE WINAPI LoadLibraryExW_Hook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) {
    HMODULE module = LoadLibraryExW_Original(lpLibFileName, hFile, dwFlags);
    if (module && !g_taskbarViewDllLoaded &&
        GetTaskbarViewModuleHandle() == module && !g_taskbarViewDllLoaded.exchange(true)) {
        Wh_Log(L"Taskbar View DLL loaded: %s", lpLibFileName);
        if (HookTaskbarViewDllSymbols(module)) Wh_ApplyHookOperations();
    }
    return module;
}

3. The native BackgroundElement is hidden even when no blob is drawn, and that state is never restored.

EnsureBlobOnButton calls GetElementVisual(bg).IsVisible(!isActive) at the top, before the grid lookup, the geometry pass and the binding. Every failure path after that point leaves the blob at Opacity(0.0) and the native indicator hidden, so an active button ends up with no indicator at all:

  • if (!grid) return;GetTaskbarRootGrid walks up looking for Taskbar.TaskbarFrame. Taskbar buttons in the overflow flyout live under OverflowFlyoutListRepeater in a separate XAML island (see the notes in taskbar-vertical.wh.cpp#L2378-L2380), so there is no TaskbarFrame ancestor and this returns permanently. Run enough apps to trigger overflow, then open the flyout: the active item loses its background with nothing drawn in its place.
  • the if (bW > 0.001) guard and a failed BindBlobExpression do the same, transiently or permanently.

Drive both the blob opacity and the suppression from one condition, at the end, and restore the native visual on the early-outs:

auto setShown = [&](bool show) {
    if (bg) {
        try { ElementCompositionPreview::GetElementVisual(bg).IsVisible(!show); } catch (...) {}
    }
};
...
if (!grid) { setShown(false); return; }   // native indicator comes back
...
bool show = isActive && entry->bound;
blobShape.Opacity(show ? 1.0 : 0.0);
setShown(show);

4. Stale blobs and their expression animations accumulate.

The Unloaded handler only hides the blob (blob.Opacity(0.0)) and invalidates the binding. The Path stays parented in RootGrid and its ExpressionAnimation keeps running, holding SetReferenceParameter strong references to the dead button's whole visual chain — so every removed button leaks a XAML element plus up to 15 composition visuals. Entries are only pruned once g_blobEntries->size() > 100, an arbitrary threshold which also means up to 100 stale hidden Path elements sit in the taskbar grid. In the Unloaded handler, stop the animation, unparent the blob and drop the entry (the create path already rebuilds everything when a container is reused), and drop the > 100 gate.

5. Overlap with Taskbar Elastic WinUI Pill.

Both mods replace the Windows 11 active-item indicator with a custom parametric shape, and this one reuses that mod's structure and settings vocabulary almost verbatim (Dimensions, Margins, a radius, BgOpacity, CustomColor with gradient and light | dark split). The maintainer's strong preference is to extend an existing mod rather than merge a near-duplicate. Please state explicitly in the PR how this differs from the elastic pill for a user, and consider contributing the blob geometry as a shape option there instead — coordinating with its author, since you're also building on their unreleased Elastic Border.

6. README: add a screenshot and the attribution.

This is a purely visual mod and the README has no image — add one of the images from the PR body (only i.imgur.com and raw.githubusercontent.com are allowed hosts; the github.com/user-attachments/... URLs used in the PR body are not). Also move the "based on Taskbar Elastic WinUI Pill / Taskbar Elastic Border by Lockframe" credit into the README: Windhawk mods have a single author, and extra credits belong in the README rather than only in the PR description.

7. BlobShape.Margins documents four values but only two do anything.

$name says (Left, Top, Right, Bottom) and ParseThickness parses all four, but only CustomMargin.Left and .Top are ever read, so Right/Bottom silently do nothing. Either narrow the setting to (Left, Top) or apply all four. Also note the two-value branch assigns outT = outB = vals[0]; outL = outR = vals[1];, which is the reverse of XAML's Margin="horizontal,vertical" convention.

Optional improvements

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

  • std::vector<std::shared_ptr<BlobEntry>>* g_blobEntries = new ... plus delete in Wh_ModUninit isn't needed here: BlobEntry holds only winrt::weak_refs and event_tokens, so the container's destructor is a plain heap free plus in-process weak-ref releases, which is safe to run at process shutdown. A plain global std::vector<std::shared_ptr<BlobEntry>> g_blobEntries; is simpler and avoids the dangling pointer delete leaves behind. See https://github.com/ramensoftware/windhawk/wiki/Global-objects-and-process-shutdown for when the [[clang::no_destroy]] pattern is genuinely required.
  • The sameColor fast path only covers SolidColorBrush, so a gradient configuration rebuilds a LinearGradientBrush plus its GradientStops on every UpdateVisualStates — i.e. on every hover and press. Cache the last applied color list in BlobEntry and compare that instead.
  • bg.Opacity(1.0) in the cleanup path is dead code: the mod no longer sets a local Opacity on BackgroundElement, only the hand-off visual's IsVisible.
  • The comment on BlobEntry ("The blob lives inside the button's IconPanel") contradicts the code — the blob lives in the taskbar's RootGrid.
  • Add #include <string> and #include <string_view>: the file uses std::wstring, std::to_wstring, std::stod/std::stoul and std::wstring_view but relies on them arriving transitively.
  • Sleep(50); // Let layout settle at the end of Wh_ModBeforeUninit blocks the Windhawk engine thread on a guess; the cleanup work is already awaited by the event above it.
  • IsButtonActive duplicates the state lookup that the hook lambda does inline — the hook could just call IsButtonActive(button).
  • elem.Dispatcher() isn't null-checked in the hook before RunAsync, unlike the other call sites.
  • ParseHexColor uses std::stoul, which stops at the first invalid digit instead of failing, so #FFxyz123 silently parses as 0xFF instead of falling back to the accent color. Validating that all characters are hex digits first would be more predictable.

Functionality notes

Non-critical observations about the feature behavior itself.

  • Hiding the whole BackgroundElement visual also removes hover and pressed feedback for the active button, since CommonStates animates that same element — hovering the focused app's button gives no visual response while the blob is shown. The elastic pill instead hides Rectangle#RunningIndicator and leaves the background alone (taskbar-elastic-pill.wh.cpp#L1454-L1462). Worth checking whether that trade-off is what you want.
  • Relatedly, the native RunningIndicator line is left visible and draws over the blob (the blob is inserted below the repeater in z-order). Intentional?
  • Light/dark colors are resolved from Application::Current().RequestedTheme() inside EnsureBlobOnButton, so a theme switch only takes effect per button on its next visual-state or size change; untouched buttons keep the previous theme's fill.
  • Buttons in the taskbar overflow flyout never get a blob at all (no Taskbar.TaskbarFrame ancestor — see item 3). Once the suppression bug is fixed they will simply show the native indicator, which is a reasonable fallback, but it's worth a line in the README.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Jul 30, 2026
Deen-0x added 6 commits July 30, 2026 17:44
1. VEH/probing removed
2.  Loader hook inlined.
3. Suppression driven by the final state.
4. Unloaded is a full teardown.
7. Margins honor all four values.
1. Global container — plain std::vector<std::shared_ptr<BlobEntry>> g_blobEntries;, all -> sites converted, delete gone; Wh_ModUninit is now just the log line. The reviewer's reasoning holds: nothing in the entries has cross-process or destructor-order hazards.

2. Gradient fast path — BlobEntry gained lastColors; the fill comparison is now an element-wise ARGB compare against the last applied list (guarded by blobShape.Fill() for the recreated-blob case), so gradient configs no longer rebuild a LinearGradientBrush plus stops on every hover and press.

3. Dead bg.Opacity(1.0) removed from cleanup — correct call; it was a leftover belt-and-suspenders from the local-opacity era.

4. BlobEntry comment now says RootGrid hosting + expression glue. That stale line was a fossil from the IconPanel iteration.

5. Includes — <string>, <string_view>, and <cwctype> (the latter for item 9's iswxdigit) added explicitly.

6. Sleep(50) dropped; the event wait above it is the actual synchronization.

7. Hook deduplicated — the inline state lookup became EnsureBlobOnButton(button, IsButtonActive(button), localSettings). GetVisualStateGroup keeps its one remaining caller inside IsButtonActive.

8. Dispatcher null-check added in the hook before RunAsync, matching the other call sites.

9. ParseHexColor validates every character with iswxdigit before std::stoul, so #FFxyz123 now falls through to the accent fallback instead of silently parsing as 0xFF.
RunningIndicator suppression. EnsureBlobOnButton now also resolves RunningIndicator in the IconPanel, and setNativeHidden toggles both elements' hand-off IsVisible from the single show condition. So: blob shown → background and indicator line hidden; blob not shown (inactive, unbound, overflow flyout) → both fully native. Restoration was added everywhere the background is restored — the Unloaded teardown and the uninstall cleanup — so disable/uninstall brings the line back on every button.

Theme handling. Two changes, as flagged plus a deeper issue:

IsElementLightMode is back (element ActualTheme first, RequestedTheme only for the Default fallback), and GetBlobShapeColors now takes isLight from the caller, resolved from the button. This fixes the correctness problem — RequestedTheme is frozen at process start, so after a live theme switch it wasn't just stale, it was wrong for as long as the session lasted.
Each blob subscribes to ActualThemeChanged at creation (token in the entry, detached in both teardown paths). A theme switch propagates through the tree, every blob's handler fires on its own UI thread, re-runs the ensure, and the lastColors comparison from the previous round turns that into exactly one brush swap per blob — including inactive/hidden ones, so nothing shows the old theme's fill when it next activates.
@Deen-0x

Deen-0x commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Jul 31, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


Thanks for working through the previous round — the VEH probing, the deferred-hook thread, the suppression logic, the Unloaded teardown and the theme handling all look properly fixed. Two things left.

1. Buttons that never get a blob keep their Loaded/Unloaded handlers attached after the mod unloads.

EnsureBlobOnButton attaches the Unloaded (line 691) and Loaded (line 753) handlers before the RootGrid lookup at line 769-780. When that lookup fails the function returns with an entry that has both handlers subscribed and blobShape == nullptr.

Wh_ModBeforeUninit derives the cleanup dispatcher exclusively from the blob:

auto dispatcher = blobShape ? blobShape.Dispatcher() : nullptr;
if (dispatcher) { /* ... cleanup ... */ }
else { /* just decrements `pending` */ }

So for those entries cleanup() — which is the only place btn.Unloaded(entry->unloadToken) / btn.Loaded(entry->loadedToken) are revoked — never runs. The handlers stay subscribed to lambdas living in the mod DLL, and fire after it has been unloaded → Explorer crash.

This isn't hypothetical: the taskbar overflow flyout hosts TaskListButtons under OverflowFlyoutListRepeater, which has no Taskbar.TaskbarFrame ancestor (see the notes in taskbar-vertical.wh.cpp#L2377-L2381 and taskbar-labels.wh.cpp#L1209-L1217), so GetTaskbarRootGrid returns nullptr for every one of them, permanently. Repro: run enough apps to trigger overflow, open the flyout once, disable the mod, then open/close the flyout again.

cleanup() already handles a null blobShape correctly — only the dispatcher resolution needs fixing:

auto blobShape = entry->blobShape.get();
auto btn = entry->button.get();
...
auto dispatcher = blobShape ? blobShape.Dispatcher()
                            : (btn ? btn.Dispatcher() : nullptr);

taskbar-elastic-pill.wh.cpp#L2044-L2076 does exactly this for its button event tokens (button.Dispatcher()), which is why it doesn't have the problem.

Wh_ModSettingsChanged has the same blind spot (if (!dispatcher) continue;) — harmless there, but the same fallback makes those buttons re-attempt their blob on a settings change instead of waiting for the next UpdateVisualStates.

2. Overlap with Taskbar Elastic WinUI Pill (unresolved from the previous round).

The attribution in the README is appreciated, but the underlying question is still open: both mods replace the Windows 11 active-item indicator with a custom parametric shape, and this one shares that mod's structure and settings vocabulary (Dimensions, Margins, a radius, BgOpacity, CustomColor with gradients and light | dark). The blob geometry and the per-button-shape hosting model are a genuine difference, but the maintainer's standing preference is to extend an existing mod rather than merge a near-duplicate. Contributing the blob as an additional shape option in the elastic pill (coordinating with its author, whose unreleased Elastic Border you're also building on) would be the preferred outcome. Ultimately this is the maintainer's call, not a code defect.

Optional improvements

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

  • Dispatcher priority mismatch on unload. Wh_ModSettingsChanged posts its work at CoreDispatcherPriority::Low (line 1099) while the hook and the uninit cleanup both post at High. CoreDispatcher is priority-ordered, so a High item queued later runs before a Low item queued earlier — meaning the WaitForSingleObject barrier in Wh_ModBeforeUninit can complete while settings-change lambdas are still queued, and they then execute in an unloaded DLL. The window is tiny (change a setting, disable the mod immediately), but the fix is cheap: either post settings-change work at High too, or add the trailing per-dispatcher Low barrier item that the elastic pill uses for exactly this reason — taskbar-elastic-pill.wh.cpp#L2081-L2100.
  • Wh_ModBeforeUninit silently ignores a WAIT_TIMEOUT from the 2 s wait. Worth logging it like taskbar-elastic-pill.wh.cpp#L2105-L2108 does — a timeout is precisely the case where a later crash needs explaining.
  • The orphan-pruning path in FindOrCreateEntry (line 565-571) unparents the stale blob but doesn't StopAnimation(L"Translation") first, unlike the Unloaded teardown and the uninit cleanup. Adding it makes the three teardown paths consistent and releases the expression's references to the dead visual chain deterministically.
  • FindOrCreateEntry walks the whole entry list resolving a weak_ref per element on every UpdateVisualStates (i.e. every hover/press), and IsButtonActive + EnsureBlobOnButton each do their own recursive FindChildByName(button, L"IconPanel") walk. Resolving IconPanel once and passing it down, and/or stashing the entry on the button (Tag) instead of scanning, would cut most of that.
  • blobShape.Margin(FromLengths(0, 0, -1000, -1000)) (line 790) is a layout-suppression trick that isn't obvious from the code — a one-line comment saying it zeroes the Path's contribution to the grid's desired size would help future readers.
  • GetProcAddress(kb, ...) (line 1011) is called without checking kb. kernelbase.dll is always loaded so it can't fail in practice, but the null check is free.
  • Colors.BgOpacity / $name: Background opacity reads as if it affects the taskbar background; it's the blob fill's alpha multiplier. Something like "Blob opacity (Light, Dark)" would be clearer.

Functionality notes

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

  • Suppressing both BackgroundElement and RunningIndicator on the active button also removes its hover and pressed feedback, since CommonStates animates BackgroundElement. That's a coherent choice for this design (the blob replaces the background outright), but it means the foreground app's button no longer reacts to the pointer — worth a line in the README so users aren't surprised.
  • Buttons in the overflow flyout never get a blob (item 1 above — no Taskbar.TaskbarFrame ancestor). They correctly fall back to the native indicator, which is a fine outcome, but it's another README-worthy limitation.
  • When the mod is enabled while Explorer is already running, existing buttons only get their blob on their next UpdateVisualStates, so the currently-active button keeps the native indicator until something changes state. It self-heals within a mouse move over the taskbar, and the elastic pill behaves the same way, so this is just an FYI.
  • Each button gets its own Path plus its own running ExpressionAnimation (referencing up to 15 ancestor visuals), even though at most one blob is ever visible per taskbar. The elastic pill instead keeps a single shape per taskbar and slides it. Your approach avoids retargeting the expression on every activation, which is a reasonable trade, but with a busy taskbar it's N idle animations on the render thread where 1 would do.
  • The Y anchor is the RootGrid origin, so the shape assumes a bottom-edge taskbar of roughly the stock height — the Height: 36 default has to be re-tuned by hand for anyone using a taskbar-height or taskbar-position mod. The setting description covers it; just noting the assumption is baked into the anchoring, not only the default.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Jul 31, 2026
Deen-0x added 2 commits July 31, 2026 23:01
Wh_ModBeforeUninit — btn is resolved alongside the blob, and the dispatcher falls back to btn.Dispatcher() when the blob is null. The cleanup lambda didn't need touching, exactly as the reviewer said: its if (blobShape) guard and separate button branch (which is where the Unloaded/Loaded token revocations live) already do the right thing when there's no blob — it just never ran for those entries. I added a comment spelling out why blob-less entries exist, so nobody "simplifies" this back into the crash later.

Wh_ModSettingsChanged — same fallback. Harmless before, but as the reviewer noted, it upgrades behavior for free: flyout-hosted buttons (and any not-yet-rooted button) now re-attempt on a settings change instead of waiting for their next UpdateVisualStates.
1. Priority inversion — Wh_ModSettingsChanged now posts at High like the hook and cleanup, so within each dispatcher everything is FIFO relative to the uninit barrier and nothing can be left queued behind it. I went with the reprioritization rather than elastic-pill's trailing-Low-barrier approach because after this change no mod code posts at Low — the barrier would guard an empty class. If a future change ever posts at Low again, the barrier becomes the required companion; the comment says so.

2. Timeout logging — WAIT_TIMEOUT from the 2 s wait now logs "Timed out waiting for blob shape cleanup". As the reviewer put it, that's exactly the breadcrumb a post-unload crash report needs.

3. Orphan pruning — now StopAnimation(L"Translation") before unparenting, matching the other two teardown paths. All three teardowns are symmetric.

4. Walk deduplication — new RefreshBlob(button, settings) is the single entry point for all six triggers: it resolves IconPanel once, reads the indicator state from it, and passes both into EnsureBlobOnButton (whose signature gained the iconPanel parameter). IsButtonActive is gone, folded in — that halves the recursive walks per hover/press. The Tag-stashing half I deliberately declined: Tag on a TaskListButton is shell-owned property surface, and stomping it risks colliding with whatever the shell (or another mod) stores there; the alternative — a raw-pointer map — reintroduces stale-pointer aliasing when recycled memory gets reused. The weak-ref scan is self-validating and O(number of buttons) with trivial per-element cost, so I kept it and would defend that in the PR reply.

5. Margin trick documented — three-line comment on the (0, 0, -1000, -1000) margin explaining it cancels the Path's contribution to the grid's desired size.

6. kernelbase null check — GetProcAddress is guarded and a null function pointer now fails init through the same logged path.

7. Opacity naming — display name is "Blob opacity (Light, Dark)" with a description that says fill opacity; the BgOpacity key is unchanged so existing users' values survive.
@Deen-0x

Deen-0x commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Regarding Point 2:
Fair question - this did begin as a fork of that lineage, and the settings vocabulary is deliberately inherited. But the shapes are topologically different, and that forced a different implementation.

The elastic pill is a convex rounded rectangle sliding behind the active button. The blob is a tab silhouette with concave top corners and a flat top edge anchored flush to the taskbar's top — it reads as the button merging into the taskbar surface, not as an indicator behind it. No parameter values turn one into the other: a Border's CornerRadius can't produce concave corners, and the flares extend beyond the button's bounds.

That geometry dictates the architecture: a Path with programmatic geometry instead of a styled Border; hosted in RootGrid above the task list's clip region (button-hosted shapes get their flares clipped — verified); one static shape per button toggled by indicator state instead of one shared shape animating between them, each glued to its button with an X-only composition expression while Y is structurally anchored to the taskbar top. Merging this into the pill wouldn't be a shape enum — it's a second rendering element, hosting model, and lifecycle model behind a mode switch, sharing little beyond the hook.

Regarding functionality notes:

  1. Windows already has press icon sizing feedback, there's no need for hover feedback here imo.

  2. That's intended, a blob isn't designed to be displayed in overflow, it would look weird and unintentional.

  3. It's good and tbh intended that the blob behaves the same way.

  4. After extensive testing, this solution proved to be more consistent and bug free (no stuttering and animation laggs)

  5. Intended solution to combat y axis stuttering.

@Deen-0x

Deen-0x commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-ai-review An AI review was requested and is being prepared. label Jul 31, 2026
@windhawk-reviewer

Copy link
Copy Markdown

New commits were pushed, so this pull request left the human review queue and is back to waiting-for-author.

Comment /ai-review to get an AI review of the updated code, then /ready-for-reviewer to hand it over to a human reviewer again. See the pull request review process for details.

split restore: on deactivation, restore the RunningIndicator immediately (so the running dash appears correctly), but bring BackgroundElement back on a short one-shot delay (~400 ms) that outlives the transition storyboards — and cancel the pending restore if the button re-activates first, so rapid task switching never flashes. This is the legitimate use of a timer (an actual time-based phenomenon — animation duration), and it follows the discipline we established: one-shot, never re-arms itself, stopped on every teardown path.
@Deen-0x

Deen-0x commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 1, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 2 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-01 19:35 UTC (in 2 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 1, 2026
@Deen-0x

Deen-0x commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 1, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


The mod is in good shape overall — the lifecycle handling, the unload barrier, the kernelbase!LoadLibraryExW hook and the settings/code mapping all look right. Two things need attention: a regression introduced by the new DispatcherTimer, and the relationship to taskbar-elastic-pill.

1. BlobEntry::restoreTimer makes g_blobEntries unsafe to destroy at process shutdown. Since the delayed-restore timer was added, BlobEntry holds a strong winrt::Windows::UI::Xaml::DispatcherTimer (line 139), so the plain global std::vector<std::shared_ptr<BlobEntry>> (line 154) is now a container of thread-affine XAML objects. Wh_ModUninit does not run when explorer.exe itself terminates (Explorer restart, sign-out, reboot) — only the CRT destructors of globals do, on the shutdown thread, after every UI thread has already been killed. ~vector then releases every DispatcherTimer off the UI thread with the XAML core already torn down, which is exactly the "XAML objects released off the UI thread, after XAML teardown: crash" case. (The earlier conclusion that the entries were destructor-safe was correct at the time — weak_ref, event_token and vectors of winrt::Windows::UI::Color are all safe; the timer changed that.)

Fix — suppress the automatic destructor and keep the explicit release:

[[clang::no_destroy]] std::optional<std::vector<std::shared_ptr<BlobEntry>>>
    g_blobEntries{std::in_place};

with the accesses becoming g_blobEntries->..., and g_blobEntries.reset() (not .clear(), which keeps the buffer) in place of the clear() at line 1129. Note that the Unloaded handler's erase (lines 814-818) checks g_unloading outside the lock, so after this change it needs an engaged check (if (!g_blobEntries) return;) under g_blobEntriesMutex before dereferencing. See https://github.com/ramensoftware/windhawk/wiki/Global-objects-and-process-shutdowncase 5, containers of resource-owning or thread-affine elements. tray-utility-customizer.wh.cpp#L1117 does the same for its DispatcherTimer globals.

2. The restore timer is not stopped on three of its teardown paths. In both teardown paths the Stop() sits inside the "button is still alive" branch — Wh_ModBeforeUninit's cleanup lambda (lines 1158-1167) and the Unloaded handler (lines 801-804) — and FindOrCreateEntry's orphan pruning (lines 600-609) erases entries without touching the timer at all. All three of those paths are reached precisely when entry->button has expired, which is also when the timer is most likely to be armed: ScheduleBgRestore starts a 100 ms timer on deactivation, and closing an app (or moving its window to another monitor) destroys the button right after. A started DispatcherTimer is kept alive by the dispatcher, so dropping the entry does not stop it — and the Tick handler's if (!e) return; (line 573) fires before the Stop() on line 574, so once the entry is gone the timer never stops itself either. It then keeps ticking into a delegate whose code lives in the mod DLL; when the mod is disabled or updated, that tick lands in an unloaded image and takes Explorer down with it.

Fix, three parts:

  • Make the tick self-stopping regardless of the entry, using the sender — same as taskbar-ai-quota.wh.cpp#L2884:

    timer.Tick([weakEntry](auto const& sender, auto const&) {
        if (auto t = sender.try_as<winrt::Windows::UI::Xaml::DispatcherTimer>()) t.Stop();
        auto e = weakEntry.lock();
        if (!e || g_unloading || !e->bgHidden) return;
        ...
    });
  • Hoist the stop out of the if (btn) branches in both teardown paths, and add one to the prune loop.

  • Release the reference there too, not just stop it: entry->restoreTimer = nullptr;. Both teardown paths run on the UI thread, but the entries themselves are also released on the Windhawk thread — localEntries in Wh_ModBeforeUninit goes out of scope there, and whichever of that vector and the posted cleanup lambda holds the last shared_ptr runs ~BlobEntry. Nulling the timer inside the UI-thread cleanup guarantees the entry can never carry a strong XAML reference back across threads.

3. Overlap with taskbar-elastic-pill. The attribution in the README is appreciated, but the relationship is closer than "based on": the same TaskListButton::UpdateVisualStates hook, the same Taskbar.TaskbarFrameRootGrid resolution, verbatim ParseHexColor / ParseDoublePair / ParseGradientColorPair, the same Dimensions / Margins / BgOpacity / CustomColor settings vocabulary, the same loader hook and the same uninit barrier. Both mods do the same user-visible job — replace the active taskbar item's indicator with a custom parametric shape — and they will fight each other if both are enabled, since both suppress BackgroundElement/RunningIndicator and inject their own element.

The maintainer's standing preference is to extend an existing mod rather than merge a close variant, since duplicates fragment the catalog and make it harder for users to pick. taskbar-elastic-pill already parameterizes the pill's geometry, so a Shape: pill | blob option there (contributed upstream to Lockframe) would cover this without a second mod. If you think the blob warrants standing alone — the tab geometry with concave flares anchored to the taskbar top, the per-button static shape instead of one sliding pill, and no animation surface are real differences — please make that case in the PR description so the maintainer can weigh it. Either way it's a call worth making explicitly rather than by default.

Optional improvements

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

  • -loleaut32 in @compilerOptions looks unused — there's no BSTR/VARIANT/SAFEARRAY usage anywhere in the file. (It's inherited from taskbar-elastic-pill, which doesn't need it either.)
  • entry->anchor does double duty as "the element whose size we track" and "the element whose visual we hid" (see the comment on line 576). They happen to be the same object today because anchor = bg ? bg : iconPanel, but the delayed restore reads anchor while setNativeHidden hid bg — if BackgroundElement is ever re-templated between the hide and the tick, the restore targets the wrong element and the old one stays invisible. A dedicated winrt::weak_ref<FrameworkElement> bgElement in the entry would decouple the two for a couple of lines of code.
  • RefreshBlob does a recursive FindChildByName(button, L"IconPanel") on every trigger, and EnsureBlobOnButton then walks it again twice for BackgroundElement and RunningIndicator. Since the entry already caches the grid and anchor, caching weak refs to those three per entry (re-resolved when they expire) would remove the walks from the hover/press path entirely.
  • In the settings block, BottomRadius is listed before TopRadius; swapping them would match the top-to-bottom order used in the README's description of the shape.
  • GetVisualStateGroup now has exactly one caller (RefreshBlob); it could be folded in, though keeping it named is fine too.

Functionality notes

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

  • Blobs only appear once UpdateVisualStates next fires for a button. When the mod is enabled mid-session g_blobEntries is empty, so nothing — not even a settings change, which iterates existing entries — creates a blob for the currently active button; the user has to focus another window or hover the taskbar first. taskbar-elastic-pill behaves the same way, so this isn't a regression, but a one-time walk of the taskbar tree (from Wh_ModAfterInit, posted to the UI thread) applying RefreshBlob to the existing TaskListButtons would make enabling the mod feel instant.
  • Every button keeps its own Path with a live ExpressionAnimation even while hidden at Opacity(0.0) — only one button is ever active, so with a full taskbar that's ~20 per-frame expression evaluations on the render thread doing nothing. Stopping the animation when show goes false (and rebinding on the next activation, which the bound flag already supports) would cut that to one. Probably not measurable, just noting it.
  • ScheduleBgRestore calls Start() on an already-running timer, which restarts the 100 ms countdown. Any UpdateVisualStates on a still-inactive button (pointer enter/leave, press) therefore pushes the background restore back another 100 ms, so hovering a button right after deactivating it leaves it with neither the blob nor its native background for longer than intended. Guarding with if (!entry->restoreTimer.IsEnabled()) entry->restoreTimer.Start(); would make the delay a fixed 100 ms from deactivation.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 1, 2026
Item 1 — shutdown-safe container. g_blobEntries is now [[clang::no_destroy]] std::optional<std::vector<...>>{std::in_place}, all eight accesses converted to ->, and Wh_ModBeforeUninit does std::move(*g_blobEntries) + reset() instead of copy + clear(). Engaged checks added everywhere a disengaged optional could be reached: the Unloaded handler's erase (under the lock, as the reviewer specified — its g_unloading check is indeed outside), FindOrCreateEntry (folded into the existing unloading bail), and Wh_ModSettingsChanged. The reviewer's framing is exactly right: my earlier "plain global is safe" analysis was true of what BlobEntry contained then, and adding the DispatcherTimer silently invalidated it — thread-affine members change the container's shutdown class.

Item 2 — the timer can no longer outlive its teardown. All three parts:

Tick self-stops via the sender (sender.try_as<DispatcherTimer>().Stop()) before touching the entry — so even a timer whose entry is gone goes quiet after exactly one more tick, closing the "keeps ticking into an unloaded image" path. This was the sharpest catch in the review: my if (!e) return; sat before the Stop(), making the guard itself the leak.
Stop hoisted out of the if (btn) branches in both the Unloaded teardown and the uninit cleanup — precisely the dead-button case where the timer is most likely armed. The prune loop now collects whole entries instead of just blobs and stops each orphan's timer on its blob's dispatcher when one exists, best-effort inline otherwise (with the sender self-stop as the backstop for a failed cross-thread Stop).
Release, not just stop: restoreTimer = nullptr at all five sites, so an entry whose last shared_ptr drops on the Windhawk thread carries no strong XAML reference across threads.
@Deen-0x

Deen-0x commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Regarding the relationship to Taskbar Elastic WinUI Pill. This began as a fork of that lineage, which is why the hook, helpers, and settings vocabulary are shared — kept deliberately so users find familiar knobs. The mods diverge at the geometry, and the geometry forced a different architecture. The pill is a convex rounded Border that slides between buttons; the blob is a tab silhouette with concave top flares and a flat top edge anchored to the taskbar's top — no CornerRadius values produce it, and the flares extend beyond the button's bounds. That requires a Path with programmatic geometry, hosting in RootGrid above the task list's clip region (button-hosted shapes get their flares clipped — verified), one static per-button shape with full lifecycle tracking instead of one animated shared element, and no animation surface at all. Merging it into the pill would mean a second rendering element, hosting model, positioning model, and lifecycle model behind a mode switch — a maintenance burden on Lockframe's mod larger than a standalone mod that shares its vocabulary. That said, I defer to the maintainer and to Lockframe: if a Shape: pill | blob option upstream is preferred, I'll contribute this there instead. The mods should not be enabled together (both suppress the native indicator), which I've noted in the README — happy to also add a mutual note to the pill's README if Lockframe agrees.

@Deen-0x

Deen-0x commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 1, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

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

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

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


The shutdown-safety and timer-lifetime items from the last round are properly fixed — [[clang::no_destroy]] std::optional<...> with reset() on the controlled path, the self-stopping Tick, the stop-and-release hoisted out of the if (btn) branches in all three teardowns. The unload fence, the locked cross-thread weak-ref reads, the fail-safe InsertBlobBelowRepeater, and the per-entry bgHidden/indicatorHidden tracking all look right too. One thing left:

1. The README doesn't carry the conflict warning. In the PR discussion you wrote that the mods "should not be enabled together (both suppress the native indicator), which I've noted in the README" — but the README block at head (bfd6590) only has the attribution line (lines 21-22), no such note. Both mods hide BackgroundElement/RunningIndicator and inject their own element into the taskbar tree, so a user who enables both ends up with two competing indicators and an unpredictable restore when one is disabled. Since users discover mods from the catalog and won't read this PR, the warning needs to be in the README itself:

> **Note:** Do not enable this together with *Taskbar Elastic WinUI Pill* —
> both mods suppress the native indicator and inject their own shape, so they
> will fight over the same buttons.

On the consolidation question itself (fold into taskbar-elastic-pill behind a Shape: pill | blob option vs. ship standalone): your reasoning — the Path geometry, the RootGrid hosting above the clip region, the per-button static shape, no animation surface — is on record and reasonable. That's a maintainer/Lockframe call now, nothing further needed from you.

Optional improvements

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

  • Bind/unbind churn on inactive buttons. Now that deactivation stops the Translation animation and clears bound (lines 1142-1147), every later UpdateVisualStates on an inactive button (pointer enter, leave, press) walks the visual chain, builds a fresh ExpressionAnimation with one reference parameter per chain element, starts it — and is then immediately stopped again by that same block, since !isActive && entry->bound is true. Gate the bind on the condition that actually drives show:

    if (isActive &&
        (!entry->bound ||
         std::abs(entry->boundAdjX - adjX) > 0.5f ||
         std::abs(entry->boundYBase - yBase) > 0.5f)) {

    boundAdjX/boundYBase are only compared when bound is true, so nothing else needs to change.

  • bgHidden / indicatorHidden survive a change of the element they describe. Lines 801-806 and 819-824 re-resolve entry->bgElement / entry->indicatorElement when the cached weak ref expires or the element is detached, but the flags carry over unchanged. If the re-resolve ever yields a different element while the flag is still set, setNativeHidden(true)'s if (bg && !entry->bgHidden) skips hiding the new one and the native background renders on top of the blob. Very unlikely in practice (the button template isn't rebuilt under a live button), but clearing the flag when the resolved element changes is two lines and removes the invariant hole:

    auto newBg = FindChildByName(iconPanel, L"BackgroundElement");
    if (newBg != bg) entry->bgHidden = false;
  • Both restore paths re-walk instead of using the cached refs. RestoreNativeVisuals (line 527) resolves IconPanelBackgroundElement / RunningIndicator by name, while the entry already knows exactly which two elements it hid. That's the same decoupling you applied to the delayed restore — using entry->bgElement / entry->indicatorElement first and keeping the name walk only as a fallback would make every restore path target the element that was actually hidden.

  • -loleaut32 still looks unused (re-flagging from last round — no BSTR/VARIANT/SAFEARRAY anywhere in the file). -lole32 looks unused too: there's no CoInitialize/CoCreateInstance/CoTaskMem* call, and the WinRT bits you use come from -lruntimeobject. Worth a build with both dropped.

  • Wh_ModBeforeUninit's per-entry body isn't exception-guarded. blobShape.Dispatcher() / btn.Dispatcher() / dispatcher.RunAsync(...) (lines 1306-1317) are outside any try, so a throwing call there escapes the Windhawk callback and leaves pending un-decremented, costing the full 2 s wait. taskbar-elastic-pill has the same shape, so it's not a regression, but wrapping the loop body in try { ... } catch (...) { /* decrement + log */ } makes the teardown finish deterministically.

Functionality notes

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

  • The repeater lookup assumes TaskbarFrameRepeater is a direct child of RootGrid. Both InsertBlobBelowRepeater (lines 545-556) and SweepExistingButtons (lines 649-658) scan grid.Children() only. Every other taskbar mod in the catalog resolves that element with a recursive FindChildByNametaskbar-labels#L1427, taskbar-fluent-media-player#L7094, taskbar-multirow#L645 — which suggests the flat nesting isn't guaranteed across builds. If a build ever wraps the repeater, the mod silently does nothing beyond one log line. The direct-child scan is still what the index insertion needs, but you can have both: resolve the repeater recursively, then walk back up to whichever ancestor is a direct RootGrid child and insert at that one's index. Fail-safe behavior is unchanged when neither is found.

  • The 100 ms in ScheduleBgRestore is a fixed guess at the shell's deactivation storyboard length. Longer transitions in a future build bring the flash back; shorter ones leave the button without its hover highlight for the tail of the window. There's no clean event to hang this off (the storyboards belong to the shell), so this is just an FYI — a one-line comment recording where the number came from would help whoever has to retune it.

  • Mid-session enable still needs one event to get going. SweepExistingButtons fires on the first trigger from any button on a taskbar, which covers the case well in practice — but until something happens on that taskbar (hover, focus change) the active button stays bare. A single sweep per taskbar posted from Wh_ModAfterInit would make enabling instant. Low value given the sweep, just noting it's still there.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 1, 2026
@Deen-0x

Deen-0x commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 1, 2026
@m417z
m417z merged commit e5d8d9d into ramensoftware:main Aug 3, 2026
5 checks passed
@windhawk-reviewer windhawk-reviewer Bot removed the waiting-for-reviewer Ready for a human reviewer, and in the queue for one. label Aug 3, 2026
@Deen-0x
Deen-0x deleted the add-taskbar-blob-shape-mod branch August 3, 2026 20:35
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