fix(v3/windows): HiddenOnTaskbar click-through bug (WS_EX_NOACTIVATE → WS_EX_TOOLWINDOW) - #5371
Conversation
…→ WS_EX_TOOLWINDOW) WS_EX_NOACTIVATE prevents the window from being activated entirely, which blocks keyboard focus and causes stale mouse capture after Win+D or Alt+Tab (the WebView2 host keeps capturing clicks even when other apps are in focus). WS_EX_TOOLWINDOW is the correct Win32 style for hiding a window from the taskbar: it omits the window from the taskbar strip and the Alt+Tab switcher while still allowing normal activation, keyboard focus, and input. Also adds SetLayeredWindowAttributes call after SetWindowPos in setPhysicalBounds so that WS_EX_LAYERED windows (frameless+transparent or IgnoreMouseEvents) get their hit-test region refreshed on resize. Without this refresh, Windows freezes the layered hit-test region at creation time and the expanded area becomes permanently click-through. Fixes: #4761 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
WalkthroughThis PR adds SetLayeredWindowAttributes to the w32 wrapper, defines its flags, switches HiddenOnTaskbar handling to WS_EX_TOOLWINDOW, and refreshes layered-window hit-test regions after repositioning. ChangesWindows Window Styling and Layering
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.1)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes a Windows-specific input/focus bug when HiddenOnTaskbar: true is used in v3 windows, preventing post Win+D / Alt+Tab “click-through / stale mouse capture” behavior by using the correct extended window style and ensuring layered-window hit-test regions refresh after resizes.
Changes:
- Switch
HiddenOnTaskbarfromWS_EX_NOACTIVATEtoWS_EX_TOOLWINDOWso the window can still activate/focus normally while staying off the taskbar/Alt+Tab. - Refresh layered-window attributes after
SetWindowPosinsetPhysicalBounds()to ensure the layered region tracks resizes. - Add Win32 wrapper + constants for
SetLayeredWindowAttributes(LWA_ALPHA,LWA_COLORKEY).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| v3/pkg/w32/user32.go | Adds a SetLayeredWindowAttributes syscall wrapper. |
| v3/pkg/w32/constants.go | Adds LWA_ALPHA / LWA_COLORKEY constants for layered window attributes. |
| v3/pkg/application/webview_window_windows.go | Uses WS_EX_TOOLWINDOW for HiddenOnTaskbar and refreshes layered attributes after resizing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@v3/pkg/application/webview_window_windows.go`:
- Around line 707-712: The DPI-change branch that resizes the HWND directly (the
WM_DPICHANGED handler code path around the current direct SetWindowPos call)
must use the same post-SetWindowPos layered-region refresh as
setPhysicalBounds() does; after you route the WM_DPICHANGED resize through the
same helper or immediately after the SetWindowPos in that branch, check
WS_EX_LAYERED with w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE) and call
w32.SetLayeredWindowAttributes(w.hwnd, 0, 255, w32.LWA_ALPHA) to refresh the
hit-test region for transparent/IgnoreMouseEvents windows (same logic used in
setPhysicalBounds()).
- Around line 707-712: The SetLayeredWindowAttributes call is currently made for
any WS_EX_LAYERED window, which breaks per-pixel alpha masks created by
setWindowMask() (which uses UpdateLayeredWindow); modify the logic around the
SetLayeredWindowAttributes call so you only call it when the window is truly
using simple alpha (transparent or ignore-mouse-events) and not when a
mask/backing was applied by setWindowMask(), or alternatively reapply the mask
after SetWindowPos; specifically detect the mask-backed state (the same
condition setWindowMask() uses) before calling SetLayeredWindowAttributes, or
call the mask reapplication routine that uses UpdateLayeredWindow after
SetWindowPos instead of SetLayeredWindowAttributes, leaving
SetLayeredWindowAttributes for only non-mask WS_EX_LAYERED windows.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 75e452cd-5798-48a0-80bf-6becafce6b54
📒 Files selected for processing (3)
v3/pkg/application/webview_window_windows.gov3/pkg/w32/constants.gov3/pkg/w32/user32.go
After a DPI monitor transition the HWND is resized via SetWindowPos in the WM_DPICHANGED handler. For layered windows (frameless+transparent, IgnoreMouseEvents) this left the hit-test region frozen at the pre-DPI size, making the newly exposed border area permanently click-through. Apply the same SetLayeredWindowAttributes(LWA_ALPHA) refresh that was added to setPhysicalBounds() so that both resize paths keep the hit-test region in sync with the window dimensions. Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
v3/pkg/application/webview_window_windows.go (1)
706-712:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMask-backed layered windows:
SetLayeredWindowAttributeswill break per-pixel alpha set bysetWindowMask().
setWindowMask()(Line 1867) configuresWS_EX_LAYEREDand usesUpdateLayeredWindow(Line 1904) for per-pixel alpha. Per Microsoft docs, callingSetLayeredWindowAttributeson such a window causes subsequentUpdateLayeredWindowcalls to fail untilWS_EX_LAYEREDis cleared and re-set. SinceWindowMaskis processed inrun()(Line 554) and the window remainsWS_EX_LAYEREDafterward, any latersetPhysicalBounds()call (e.g.,setSize,setPosition,setBounds) will silently break the mask's alpha rendering.Gate the refresh to non-mask layered windows, e.g., skip when
w.parent.options.Windows.WindowMask != nil, or re-apply the mask afterSetWindowPosinstead of callingSetLayeredWindowAttributes.🛡️ Suggested guard
- if exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE); exStyle&w32.WS_EX_LAYERED != 0 { - w32.SetLayeredWindowAttributes(w.hwnd, 0, 255, w32.LWA_ALPHA) - } + // Skip when the layered window is driven by UpdateLayeredWindow (window mask), + // since SetLayeredWindowAttributes is incompatible with per-pixel alpha masks. + if w.parent.options.Windows.WindowMask == nil { + if exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE); exStyle&w32.WS_EX_LAYERED != 0 { + w32.SetLayeredWindowAttributes(w.hwnd, 0, 255, w32.LWA_ALPHA) + } + }🤖 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 `@v3/pkg/application/webview_window_windows.go` around lines 706 - 712, The call to SetLayeredWindowAttributes in the SetWindowPos/hit-test refresh path breaks per-pixel alpha for mask-backed layered windows created by setWindowMask() (which uses UpdateLayeredWindow); change the logic in the block that checks WS_EX_LAYERED (the code calling SetLayeredWindowAttributes) to skip calling SetLayeredWindowAttributes when the window has a mask (w.parent.options.Windows.WindowMask != nil) and instead either re-apply the mask after resizing/moving (call the same mask application used in setWindowMask/UpdateLayeredWindow) or only call SetLayeredWindowAttributes for non-mask layered windows so setPhysicalBounds()/setSize()/setPosition()/setBounds() won’t invalidate the per-pixel alpha.
🧹 Nitpick comments (1)
v3/pkg/application/webview_window_windows.go (1)
1699-1702: ⚡ Quick winDPI-path refresh added — resolves the prior concern, but inherits the mask-incompatibility.
The DPI-resize branch now performs the same layered refresh as
setPhysicalBounds(), addressing the earlier feedback thatWM_DPICHANGEDleft the hit-test region stale across monitor moves. However, this path has the sameUpdateLayeredWindow/SetLayeredWindowAttributesincompatibility for mask-backed windows flagged on Lines 706-712. Apply the sameWindowMask == nil(or equivalent mask-detection) guard here, or extract a shared helper so both call sites stay in lock-step.♻️ Suggested shared helper
+func (w *windowsWebviewWindow) refreshLayeredHitTestIfNeeded() { + // SetLayeredWindowAttributes is incompatible with UpdateLayeredWindow-based + // per-pixel alpha (used by setWindowMask), so skip it when a window mask is configured. + if w.parent.options.Windows.WindowMask != nil { + return + } + if exStyle := w32.GetWindowLong(w.hwnd, w32.GWL_EXSTYLE); exStyle&w32.WS_EX_LAYERED != 0 { + w32.SetLayeredWindowAttributes(w.hwnd, 0, 255, w32.LWA_ALPHA) + } +}Then call
w.refreshLayeredHitTestIfNeeded()from bothsetPhysicalBounds()and theWM_DPICHANGEDbranch.🤖 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 `@v3/pkg/application/webview_window_windows.go` around lines 1699 - 1702, The DPI-resize branch duplicates the layered-window refresh but doesn't guard against mask-backed windows (the UpdateLayeredWindow/SetLayeredWindowAttributes incompatibility); extract a shared helper (e.g., w.refreshLayeredHitTestIfNeeded()) and call it from both setPhysicalBounds() and the WM_DPICHANGED branch, and ensure the helper checks WindowMask == nil (or equivalent mask detection) before calling SetLayeredWindowAttributes so both call sites stay in sync and avoid touching mask-backed windows.
🤖 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.
Duplicate comments:
In `@v3/pkg/application/webview_window_windows.go`:
- Around line 706-712: The call to SetLayeredWindowAttributes in the
SetWindowPos/hit-test refresh path breaks per-pixel alpha for mask-backed
layered windows created by setWindowMask() (which uses UpdateLayeredWindow);
change the logic in the block that checks WS_EX_LAYERED (the code calling
SetLayeredWindowAttributes) to skip calling SetLayeredWindowAttributes when the
window has a mask (w.parent.options.Windows.WindowMask != nil) and instead
either re-apply the mask after resizing/moving (call the same mask application
used in setWindowMask/UpdateLayeredWindow) or only call
SetLayeredWindowAttributes for non-mask layered windows so
setPhysicalBounds()/setSize()/setPosition()/setBounds() won’t invalidate the
per-pixel alpha.
---
Nitpick comments:
In `@v3/pkg/application/webview_window_windows.go`:
- Around line 1699-1702: The DPI-resize branch duplicates the layered-window
refresh but doesn't guard against mask-backed windows (the
UpdateLayeredWindow/SetLayeredWindowAttributes incompatibility); extract a
shared helper (e.g., w.refreshLayeredHitTestIfNeeded()) and call it from both
setPhysicalBounds() and the WM_DPICHANGED branch, and ensure the helper checks
WindowMask == nil (or equivalent mask detection) before calling
SetLayeredWindowAttributes so both call sites stay in sync and avoid touching
mask-backed windows.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0f87cc81-6fb7-48ab-b816-a91abc379850
📒 Files selected for processing (1)
v3/pkg/application/webview_window_windows.go
…s): HiddenOnTaskbar click-through bug (WS_EX_NOACTIVATE → WS_EX_TOOLWINDOW)
|
Great! |
Summary
Fixes the click-through / mouse-capture bug reported in #4761.
When
HiddenOnTaskbar: trueis set on Windows, using Win+D or Alt+Tab caused stale mouse capture: the WebView2 host kept intercepting clicks across the entire screen, making the desktop unclickable until the Wails window was clicked again (which re-triggered the issue).Root cause:
WS_EX_NOACTIVATEwas used instead ofWS_EX_TOOLWINDOW.WS_EX_NOACTIVATEprevents the window from ever being activated, leaving the WebView2 focus state inconsistent after desktop/switcher interactions.WS_EX_TOOLWINDOWis the correct Win32 style for hiding from the taskbar — it omits the window from the taskbar strip and Alt+Tab while preserving normal activation and keyboard input.Changes:
v3/pkg/application/webview_window_windows.go— replaceWS_EX_NOACTIVATEwithWS_EX_TOOLWINDOWforHiddenOnTaskbar; addSetLayeredWindowAttributesrefresh insetPhysicalBounds()forWS_EX_LAYEREDwindows (frameless+transparent,IgnoreMouseEvents) so that the hit-test region is updated after resize (without this, the region is frozen at creation size and the expanded area becomes permanently click-through)v3/pkg/w32/constants.go— addLWA_ALPHAandLWA_COLORKEYconstantsv3/pkg/w32/user32.go— addSetLayeredWindowAttributesWin32 wrapperReproducer: v3 project with
Windows: application.WindowsWindow{HiddenOnTaskbar: true}. Before fix: Win+D then click anywhere → screen unclickable. After fix: normal click behavior.Build verified:
GOOS=windows GOARCH=amd64 go build ./pkg/w32/... ./pkg/application/...— clean.Note: The reporter observed that manually setting
ExStyle = WS_EX_TOOLWINDOW(which replaces all extended styles) caused "certain windows failing to deactivate". That is a separate symptom of losing other required exStyles (e.g.WS_EX_LAYERED). The fix here usesexStyle |= WS_EX_TOOLWINDOW(adds to existing styles) which does not have that side-effect.Closes #4761.
CC @leaanthony
Summary by CodeRabbit