Skip to content

fix(v3/windows): HiddenOnTaskbar click-through bug (WS_EX_NOACTIVATE → WS_EX_TOOLWINDOW) - #5371

Merged
leaanthony merged 5 commits into
masterfrom
agent/engineer-windows/2da069b9
May 9, 2026
Merged

leaanthony merged 5 commits into
masterfrom
agent/engineer-windows/2da069b9

Conversation

@leaanthony

@leaanthony leaanthony commented May 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes the click-through / mouse-capture bug reported in #4761.

When HiddenOnTaskbar: true is 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_NOACTIVATE was used instead of WS_EX_TOOLWINDOW. WS_EX_NOACTIVATE prevents the window from ever being activated, leaving the WebView2 focus state inconsistent after desktop/switcher interactions. WS_EX_TOOLWINDOW is 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 — replace WS_EX_NOACTIVATE with WS_EX_TOOLWINDOW for HiddenOnTaskbar; add SetLayeredWindowAttributes refresh in setPhysicalBounds() for WS_EX_LAYERED windows (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 — add LWA_ALPHA and LWA_COLORKEY constants
  • v3/pkg/w32/user32.go — add SetLayeredWindowAttributes Win32 wrapper

Reproducer: 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 uses exStyle |= WS_EX_TOOLWINDOW (adds to existing styles) which does not have that side-effect.

Closes #4761.

CC @leaanthony

Summary by CodeRabbit

  • Bug Fixes
    • Improved taskbar visibility handling for hidden windows on Windows.
    • Restored reliable mouse interaction after windows are moved, resized, or during DPI changes on Windows.

…→ 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>
Copilot AI review requested due to automatic review settings May 8, 2026 14:12
@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Windows Window Styling and Layering

Layer / File(s) Summary
W32 API Constants
v3/pkg/w32/constants.go
SetLayeredWindowAttributes flag constants LWA_COLORKEY and LWA_ALPHA are defined.
W32 API Bindings
v3/pkg/w32/user32.go
SetLayeredWindowAttributes procedure binding (procSetLayeredWindowAttributes) and exported wrapper function are added to forward calls to the Windows API.
Application Window Styling
v3/pkg/application/webview_window_windows.go
HiddenOnTaskbar extended-style handling now applies WS_EX_TOOLWINDOW instead of WS_EX_NOACTIVATE; setPhysicalBounds() detects WS_EX_LAYERED windows and calls SetLayeredWindowAttributes after SetWindowPos to refresh the hit-test region; same refresh added in WM_DPICHANGED handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

  • wailsapp/wails#3691: Also modifies Windows window-management code and SetWindowPos usage in webview_window_windows.go.
  • wailsapp/wails#4492: Related changes to run() logic and window styling in the same file.

Suggested labels

Bug, Windows, v3, size:M

Poem

🐰 A window hid from taskbar's sight,
Now waves WS_EX_TOOLWINDOW bright.
When layered panes are moved with care,
SetLayeredWindowAttributes is there.
Clicks hop back where they belong!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides a clear summary, root cause analysis, detailed changes, reproducer steps, and build verification. However, the PR lacks explicit testing information (Windows platform checkbox) and the required issue reference format. Check the 'Windows' testing checkbox under 'How Has This Been Tested?' to document the testing environment and include explicit test steps.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main bug fix: replacing WS_EX_NOACTIVATE with WS_EX_TOOLWINDOW to resolve the HiddenOnTaskbar click-through issue.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/engineer-windows/2da069b9

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 HiddenOnTaskbar from WS_EX_NOACTIVATE to WS_EX_TOOLWINDOW so the window can still activate/focus normally while staying off the taskbar/Alt+Tab.
  • Refresh layered-window attributes after SetWindowPos in setPhysicalBounds() 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d1be17b and 61dea05.

📒 Files selected for processing (3)
  • v3/pkg/application/webview_window_windows.go
  • v3/pkg/w32/constants.go
  • v3/pkg/w32/user32.go

Comment thread v3/pkg/application/webview_window_windows.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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
v3/pkg/application/webview_window_windows.go (1)

706-712: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mask-backed layered windows: SetLayeredWindowAttributes will break per-pixel alpha set by setWindowMask().

setWindowMask() (Line 1867) configures WS_EX_LAYERED and uses UpdateLayeredWindow (Line 1904) for per-pixel alpha. Per Microsoft docs, calling SetLayeredWindowAttributes on such a window causes subsequent UpdateLayeredWindow calls to fail until WS_EX_LAYERED is cleared and re-set. Since WindowMask is processed in run() (Line 554) and the window remains WS_EX_LAYERED afterward, any later setPhysicalBounds() 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 after SetWindowPos instead of calling SetLayeredWindowAttributes.

🛡️ 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 win

DPI-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 that WM_DPICHANGED left the hit-test region stale across monitor moves. However, this path has the same UpdateLayeredWindow/SetLayeredWindowAttributes incompatibility for mask-backed windows flagged on Lines 706-712. Apply the same WindowMask == 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 both setPhysicalBounds() and the WM_DPICHANGED branch.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 61dea05 and 32e7e51.

📒 Files selected for processing (1)
  • v3/pkg/application/webview_window_windows.go

@leaanthony
leaanthony merged commit 2876eed into master May 9, 2026
14 of 15 checks passed
@leaanthony
leaanthony deleted the agent/engineer-windows/2da069b9 branch May 9, 2026 11:05
pull Bot pushed a commit to Mu-L/wails that referenced this pull request May 9, 2026
…s): HiddenOnTaskbar click-through bug (WS_EX_NOACTIVATE → WS_EX_TOOLWINDOW)
@sugar-is-everythong

Copy link
Copy Markdown

Great!

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.

[v3] When HiddenOnTaskbar is set to true, mouse clicks behave abnormally

4 participants