fix(overlay): apply click-through styles reliably — desktop/taskbar no longer blocked#15
fix(overlay): apply click-through styles reliably — desktop/taskbar no longer blocked#15DysektAI wants to merge 1 commit into
Conversation
…ld input windows) The passive overlay spans all screens topmost. Its WS_EX_TRANSPARENT / WS_EX_LAYERED styles were set with SetWindowLongPtr but never committed: extended-style changes are cached and only take effect after a frame-changed SetWindowPos, so depending on timing the overlay kept swallowing every click on the desktop and taskbar (issue #10). - Call SetWindowPos with SWP_FRAMECHANGED (NOACTIVATE/NOMOVE/NOSIZE/ NOZORDER) after every extended-style mutation, in both overlay windows. - Apply click-through recursively to WebView2's child input windows; hit-test transparency on the top-level window does not cover them. - Re-apply styles after each overlay navigation in case the runtime recreates input windows. - SetSize now uses SWP_NOZORDER|SWP_NOACTIVATE instead of flags=0 so the resize can no longer re-z-order or activate the overlay. Fixes #10
📝 WalkthroughWalkthroughChangesOverlay click-through handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant WebView_Loaded
participant SetWindowStyle
participant EnumChildWindows
WebView_Loaded->>SetWindowStyle: Reapply styles after navigation
SetWindowStyle->>EnumChildWindows: Enumerate descendant windows
EnumChildWindows-->>SetWindowStyle: Apply click-through styles
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
🏁 CodeAnt Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
There was a problem hiding this comment.
📝 Info: Interactable overlay SetPosition still uses uFlags=0
The PR fixes BlazorOverlay.SetSize to use SWP_NOZORDER | SWP_NOACTIVATE, but BlazorInteractableOverlay.SetPosition (src/App/View/BlazorInteractableOverlay.xaml.cs:61) still calls SetWindowPos(..., 0), which re-z-orders and can activate the window. This is likely acceptable because the interactable overlay is meant to be focusable/interactive (XAML Focusable="True", no click-through styles), unlike the passive overlay, so the activation concern that motivated the SetSize fix does not apply here. Noting it since the two files diverge on this point.
(Refers to line 61)
Was this helpful? React with 👍 or 👎 to provide feedback.
| private static void ApplyStyleToDescendants(nint parent, int gwlExStyle, nint styles) | ||
| { | ||
| NativeMethods.EnumChildWindows( | ||
| parent, | ||
| (child, _) => | ||
| { | ||
| NativeMethods.SetWindowLongPtr( | ||
| child, | ||
| gwlExStyle, | ||
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); | ||
| return true; | ||
| }, | ||
| 0 | ||
| ); | ||
| } |
There was a problem hiding this comment.
📝 Info: Redundant descendant enumeration (EnumChildWindows already recurses)
ApplyStyleToDescendants (src/App/View/BlazorOverlay.xaml.cs:123-140) manually recurses by calling itself for every child, but EnumChildWindows already visits ALL descendants of the parent (Windows enumerates a window's children AND their children). Combined, each descendant at depth d gets its extended styles set and SetWindowPos called multiple times. The operation is idempotent (bitwise OR of styles) so this is not a correctness bug, but it performs redundant work that grows with hierarchy depth. Since it re-runs on every NavigationCompleted via WebView_Loaded → SetWindowStyle (src/App/View/BlazorOverlay.xaml.cs:142-152), the extra churn is small for WebView2's shallow window tree but is unnecessary.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // WebView2 creates its own child input windows beneath the WPF host; | ||
| // hit-test transparency on the top-level window alone does not cover | ||
| // them. Apply click-through to every descendant as well. | ||
| ApplyStyleToDescendants(handle, gwlExStyle, clickThroughStyles); | ||
| } | ||
|
|
||
| private static void ApplyStyleToDescendants(nint parent, int gwlExStyle, nint styles) | ||
| { | ||
| NativeMethods.EnumChildWindows( | ||
| parent, | ||
| (child, _) => | ||
| { | ||
| NativeMethods.SetWindowLongPtr( | ||
| child, | ||
| gwlExStyle, | ||
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); |
There was a problem hiding this comment.
🔍 WS_EX_LAYERED committed on WebView2 child input windows
The click-through style bundle includes WS_EX_LAYERED and is now committed (via SWP_FRAMECHANGED) not only on the top-level window but on every descendant WebView2 window (src/App/View/BlazorOverlay.xaml.cs:101,120,129-134). For a top-level window this is safe because both overlays declare AllowsTransparency="True", so WPF already owns the layered attributes. For the WebView2 child windows, setting WS_EX_LAYERED without ever calling SetLayeredWindowAttributes/UpdateLayeredWindow can, on plain child HWNDs, suppress rendering. This should be benign here because WebView2CompositionControl renders through DirectComposition (the child HWNDs are input-only), but it is worth confirming on the reporter's dual-monitor setup that the passive scan tooltip still renders correctly after a scan, per the manual smoke test noted in the PR description.
Was this helpful? React with 👍 or 👎 to provide feedback.
| private static void ApplyStyleToDescendants(nint parent, int gwlExStyle, nint styles) | ||
| { | ||
| NativeMethods.EnumChildWindows( | ||
| parent, | ||
| (child, _) => | ||
| { | ||
| NativeMethods.SetWindowLongPtr( | ||
| child, | ||
| gwlExStyle, | ||
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); | ||
| return true; | ||
| }, | ||
| 0 | ||
| ); | ||
| } |
There was a problem hiding this comment.
EnumChildWindows enumerates all descendants of a window (not just direct children) — it recurses into grandchildren automatically. The call to ApplyStyleToDescendants(child, …) inside the callback therefore re-enumerates and re-styles every already-visited subtree. For a 4-level-deep WebView2 tree A→B→C→D, node D has SetWindowLongPtr/SetWindowPos (and therefore WM_NCCALCSIZE) called on it 4 times per SetWindowStyle() invocation, which is triggered on every navigation. The recursive line should be removed so each child window is styled exactly once.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/App/View/BlazorOverlay.xaml.cs
Line: 123-140
Comment:
`EnumChildWindows` enumerates *all* descendants of a window (not just direct children) — it recurses into grandchildren automatically. The call to `ApplyStyleToDescendants(child, …)` inside the callback therefore re-enumerates and re-styles every already-visited subtree. For a 4-level-deep WebView2 tree `A→B→C→D`, node D has `SetWindowLongPtr`/`SetWindowPos` (and therefore `WM_NCCALCSIZE`) called on it 4 times per `SetWindowStyle()` invocation, which is triggered on every navigation. The recursive line should be removed so each child window is styled exactly once.
How can I resolve this? If you propose a fix, please make it concise.| NativeMethods.SetWindowLongPtr( | ||
| child, | ||
| gwlExStyle, | ||
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); | ||
| return true; |
There was a problem hiding this comment.
The recursive
ApplyStyleToDescendants(child, …) call inside the EnumChildWindows callback is redundant: EnumChildWindows already walks the entire descendant tree on its own. Removing it makes each child's style applied exactly once and avoids exponentially many redundant WM_NCCALCSIZE messages to WebView2's internal windows on each navigation.
| NativeMethods.SetWindowLongPtr( | |
| child, | |
| gwlExStyle, | |
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | |
| ); | |
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | |
| ApplyStyleToDescendants(child, gwlExStyle, styles); | |
| return true; | |
| NativeMethods.SetWindowLongPtr( | |
| child, | |
| gwlExStyle, | |
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | |
| ); | |
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | |
| return true; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/App/View/BlazorOverlay.xaml.cs
Line: 129-136
Comment:
The recursive `ApplyStyleToDescendants(child, …)` call inside the `EnumChildWindows` callback is redundant: `EnumChildWindows` already walks the entire descendant tree on its own. Removing it makes each child's style applied exactly once and avoids exponentially many redundant `WM_NCCALCSIZE` messages to WebView2's internal windows on each navigation.
```suggestion
NativeMethods.SetWindowLongPtr(
child,
gwlExStyle,
NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles
);
NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags);
return true;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/App/View/BlazorOverlay.xaml.cs`:
- Around line 123-139: Remove the recursive ApplyStyleToDescendants call from
the EnumChildWindows callback within ApplyStyleToDescendants, since enumeration
already covers all descendants. Preserve the existing style update and
SetWindowPos behavior, then run dotnet tool restore, dotnet csharpier check .,
and dotnet build RatScanner.sln.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: cf052c1b-0c40-496b-afb2-5ba830627b30
📒 Files selected for processing (2)
src/App/View/BlazorInteractableOverlay.xaml.cssrc/App/View/BlazorOverlay.xaml.cs
| private static void ApplyStyleToDescendants(nint parent, int gwlExStyle, nint styles) | ||
| { | ||
| NativeMethods.EnumChildWindows( | ||
| parent, | ||
| (child, _) => | ||
| { | ||
| NativeMethods.SetWindowLongPtr( | ||
| child, | ||
| gwlExStyle, | ||
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); | ||
| return true; | ||
| }, | ||
| 0 | ||
| ); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Remove the redundant recursive enumeration.
EnumChildWindows already invokes its callback for every descendant; line 135 re-enumerates each subtree and repeatedly frame-refreshes deep WebView2 windows on every navigation. Remove the recursive call. (learn.microsoft.com)
Proposed fix
NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags);
- ApplyStyleToDescendants(child, gwlExStyle, styles);
return true;As per coding guidelines, run dotnet build RatScanner.sln before declaring this C# change complete.
dotnet tool restore
dotnet csharpier check .
dotnet build RatScanner.sln🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/App/View/BlazorOverlay.xaml.cs` around lines 123 - 139, Remove the
recursive ApplyStyleToDescendants call from the EnumChildWindows callback within
ApplyStyleToDescendants, since enumeration already covers all descendants.
Preserve the existing style update and SetWindowPos behavior, then run dotnet
tool restore, dotnet csharpier check ., and dotnet build RatScanner.sln.
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/App/View/BlazorOverlay.xaml.cs">
<violation number="1" location="src/App/View/BlazorOverlay.xaml.cs:135">
P2: Overlay initialization and every navigation can perform exponentially repeated `SetWindowLongPtr`/`SetWindowPos` calls because `EnumChildWindows` already traverses all descendants. Removing this explicit recursion still styles every descendant once.</violation>
</file>
<file name="src/App/View/BlazorInteractableOverlay.xaml.cs">
<violation number="1" location="src/App/View/BlazorInteractableOverlay.xaml.cs:99">
P3: SWP flag constants (`swpNoMove`, `swpNoSize`, `swpNoZOrder`, `swpNoActivate`, `swpFrameChanged`) are defined as local `const uint` inside `SetWindowStyle()`, while semantically identical constants already exist as `public const uint` fields in `BlazorOverlay.NativeMethods`. This creates a maintenance trap — if the combined flags ever need to change (e.g., adding/removing a flag for all SetWindowPos callers), one of the two sites will be missed.
Consider extracting the SWP constants to a shared location (e.g., a common `NativeMethods` file) so both overlays reference the same flags, or reference `BlazorOverlay.NativeMethods.SwpFrameChangedFlags` directly to eliminate the duplication.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles | ||
| ); | ||
| NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags); | ||
| ApplyStyleToDescendants(child, gwlExStyle, styles); |
There was a problem hiding this comment.
P2: Overlay initialization and every navigation can perform exponentially repeated SetWindowLongPtr/SetWindowPos calls because EnumChildWindows already traverses all descendants. Removing this explicit recursion still styles every descendant once.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/App/View/BlazorOverlay.xaml.cs, line 135:
<comment>Overlay initialization and every navigation can perform exponentially repeated `SetWindowLongPtr`/`SetWindowPos` calls because `EnumChildWindows` already traverses all descendants. Removing this explicit recursion still styles every descendant once.</comment>
<file context>
@@ -96,13 +107,45 @@ private void SetWindowStyle()
+ NativeMethods.GetWindowLongPtr(child, gwlExStyle) | styles
+ );
+ NativeMethods.SetWindowPos(child, 0, 0, 0, 0, 0, NativeMethods.SwpFrameChangedFlags);
+ ApplyStyleToDescendants(child, gwlExStyle, styles);
+ return true;
+ },
</file context>
| { | ||
| const int gwlExStyle = -20; // GWL_EXSTYLE | ||
| const uint wsExToolWindow = 0x00000080; // WS_EX_TOOLWINDOW | ||
| const uint swpNoMove = 0x0002; |
There was a problem hiding this comment.
P3: SWP flag constants (swpNoMove, swpNoSize, swpNoZOrder, swpNoActivate, swpFrameChanged) are defined as local const uint inside SetWindowStyle(), while semantically identical constants already exist as public const uint fields in BlazorOverlay.NativeMethods. This creates a maintenance trap — if the combined flags ever need to change (e.g., adding/removing a flag for all SetWindowPos callers), one of the two sites will be missed.
Consider extracting the SWP constants to a shared location (e.g., a common NativeMethods file) so both overlays reference the same flags, or reference BlazorOverlay.NativeMethods.SwpFrameChangedFlags directly to eliminate the duplication.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/App/View/BlazorInteractableOverlay.xaml.cs, line 99:
<comment>SWP flag constants (`swpNoMove`, `swpNoSize`, `swpNoZOrder`, `swpNoActivate`, `swpFrameChanged`) are defined as local `const uint` inside `SetWindowStyle()`, while semantically identical constants already exist as `public const uint` fields in `BlazorOverlay.NativeMethods`. This creates a maintenance trap — if the combined flags ever need to change (e.g., adding/removing a flag for all SetWindowPos callers), one of the two sites will be missed.
Consider extracting the SWP constants to a shared location (e.g., a common `NativeMethods` file) so both overlays reference the same flags, or reference `BlazorOverlay.NativeMethods.SwpFrameChangedFlags` directly to eliminate the duplication.</comment>
<file context>
@@ -96,13 +96,32 @@ private void SetWindowStyle()
{
const int gwlExStyle = -20; // GWL_EXSTYLE
const uint wsExToolWindow = 0x00000080; // WS_EX_TOOLWINDOW
+ const uint swpNoMove = 0x0002;
+ const uint swpNoSize = 0x0001;
+ const uint swpNoZOrder = 0x0004;
</file context>
Root cause
The passive scan overlay (
BlazorOverlay) spans all screens, is topmost, and is shown for the whole app lifetime. Its click-through extended styles (WS_EX_TRANSPARENT/WS_EX_LAYERED/WS_EX_NOACTIVATE) were set withSetWindowLongPtr, but Windows caches extended styles — changes only take effect afterSetWindowPoswithSWP_FRAMECHANGED, which was never called. Depending on timing, the overlay therefore kept hit-testing normally and swallowed every click on the desktop and taskbar (#10, including the desktop/right-click follow-up).Two secondary gaps made it worse:
SetSize()calledSetWindowPoswithuFlags = 0, which re-z-orders and can activate the overlay.Fix
SetWindowPos(..., SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)(bothBlazorOverlayandBlazorInteractableOverlay).SetSizenow usesSWP_NOZORDER | SWP_NOACTIVATE(keeps the XAML-declaredTopmostuntouched, never activates).Validation
dotnet build/dotnet test(205 passed) / csharpier clean.Fixes #10
Summary by cubic
Make the overlay reliably click-through so desktop and taskbar clicks work again. Commits extended-style changes and covers WebView2 child input windows. Fixes #10.
Written for commit cb1958c. Summary will update on new commits.
Greptile Summary
This PR fixes a long-standing click-through regression where the
BlazorOverlaypassive scan overlay was blocking all mouse input to the desktop and taskbar. The root cause was thatSetWindowLongPtrextended-style changes are cached by Windows and require a subsequentSetWindowPos(..., SWP_FRAMECHANGED)to take effect, which was never called.SWP_FRAMECHANGEDflush after every extended-style mutation in bothBlazorOverlayandBlazorInteractableOverlay, which is the correct and necessary fix for the cached-style problem.EnumChildWindows, and re-applies on each navigation so late-created input windows are covered.SetSize()inBlazorOverlayto useSWP_NOZORDER | SWP_NOACTIVATE, preventing accidental z-order changes and focus theft during resizes.Confidence Score: 3/5
The core click-through fix is correct but two defects in the same changeset need resolution before merging.
The SetPosition() call in BlazorInteractableOverlay still passes uFlags = 0 to SetWindowPos, so the overlay can be activated and z-reordered on every ShowOverlay() call — the same focus-steal this PR explicitly sets out to prevent. Separately, ApplyStyleToDescendants calls itself recursively inside the EnumChildWindows callback despite EnumChildWindows already walking the full descendant tree, causing each child window to receive redundant WM_NCCALCSIZE messages on every navigation event.
Files Needing Attention: Both changed files need attention: BlazorInteractableOverlay.xaml.cs for the unfixed SetPosition() flags, and BlazorOverlay.xaml.cs for the redundant recursion in ApplyStyleToDescendants.
Important Files Changed
Sequence Diagram
sequenceDiagram participant WPF as WPF Host participant BO as BlazorOverlay participant WV as WebView2 participant Win32 as Win32 (user32.dll) WPF->>BO: OnSourceInitialized BO->>Win32: "SetWindowLongPtr(WS_EX_TRANSPARENT | WS_EX_LAYERED | ...)" BO->>Win32: SetWindowPos(SWP_FRAMECHANGED) flushes cached style BO->>Win32: EnumChildWindows apply click-through to all children WPF->>BO: BlazorWebView_Initialized BO->>Win32: "SetWindowPos(left, top, w, h, SWP_NOZORDER | SWP_NOACTIVATE)" BO->>Win32: SetWindowLongPtr + SetWindowPos(SWP_FRAMECHANGED) BO->>Win32: EnumChildWindows apply to children WV->>BO: NavigationCompleted (WebView_Loaded) Note over BO,WV: WebView2 may recreate child input windows here BO->>Win32: SetWindowLongPtr + SetWindowPos(SWP_FRAMECHANGED) BO->>Win32: EnumChildWindows re-apply to any new child input windowsComments Outside Diff (1)
src/App/View/BlazorInteractableOverlay.xaml.cs, line 61 (link)SetPosition()still passesuFlags = 0toSetWindowPos, which allows Windows to re-z-order and activate the interactable overlay on everyShowOverlay()call. This is the same activation-steal bug that the PR fixes inBlazorOverlay.SetSize(), but was not applied here — activating the overlay steals focus from the game, which is the exact outcomeWS_EX_NOACTIVATEis meant to prevent.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(overlay): apply click-through styles..." | Re-trigger Greptile