feat(windows): add support for custom hit-test logic for non-client regions#5462
feat(windows): add support for custom hit-test logic for non-client regions#5462savely-krasovsky wants to merge 14 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughFrontend marks caption controls with ChangesNon-Client Region Hit Testing and Mouse Input
Sequence DiagramsequenceDiagram
participant Frontend as Frontend (appregion)
participant Runtime as Runtime (wails runtime)
participant Webview as WebviewWindow
participant WindowsImpl as Windows WndProc
participant Chromium as WebView2 (Chromium)
Frontend->>Runtime: serialize regions -> wails:non-client-region:<payload>
Runtime->>Webview: message delivered
Webview->>WindowsImpl: setNonClientHitTestRegions(regions)
WindowsImpl->>WindowsImpl: WM_NCHITTEST -> DWM first
WindowsImpl->>Chromium: GetNonClientRegionAtPoint(x,y) (fallback)
Chromium-->>WindowsImpl: region kind
WindowsImpl->>WindowsImpl: compute final HT_*
WindowsImpl->>Chromium: SendMouseInput(eventKind, point) (when forwarded)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts (1)
149-223:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftGate tracking behind explicit native opt-in to avoid global DOM-scan overhead.
This module currently starts on every Windows runtime and continuously scans all DOM nodes on mutation/scroll/resize. That adds avoidable overhead for apps not using non-client regions (the new window options default to false).
🤖 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/internal/runtime/desktop/`@wailsio/runtime/src/appregion.ts around lines 149 - 223, The module must only start DOM scanning when the app explicitly opts-in; update tryStartAppRegionTracking to require a native/JS opt-in flag (for example check window._wails?.nonClientRegionsEnabled or a new runtime-provided flag) before calling whenReady(startAppRegionTracking), and stop polling if the flag is absent so we don’t set up MutationObserver/resize/scroll handlers unnecessarily; ensure startAppRegionTracking, scheduleUpdate and updateAppRegions remain unchanged except that they are only invoked after the opt-in is true, and add a one-time listener or re-check path so if the opt-in flag appears later the tracking will start once.
🤖 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_nonclient.go`:
- Around line 349-359: Add a WM_CAPTURECHANGED branch in the window message
handler to clear the composition input capture state when the OS moves capture
elsewhere: detect w32.WM_CAPTURECHANGED and, if w.compositionInput.capturing is
true, set it to false and call w32.ReleaseCapture() (mirroring the logic in the
WM_NCLBUTTONUP and other release/error paths); update the same handler(s) that
contain the existing WM_NCLBUTTONDOWN/WM_NCLBUTTONUP logic (where w.focus(),
w.compositionInput.capturing, w32.SetCapture and w32.ReleaseCapture are used) so
routeCompositionMouseInput() will no longer see stale capture state.
In `@v3/pkg/application/webview_window.go`:
- Around line 819-824: The validation currently rejects regions with an empty
kind even though the struct treats empty kind as "maximize"; before calling
validateNonClientHitTestRegion in the loop over message.Regions (the loop that
uses i, r and calls w.Error upon error), normalize any region whose Kind (or
kind field) is empty to "maximize" so legacy payloads without kind pass
validation; apply the same normalization in the other loop that validates
message.Regions later (the second block that also iterates and calls
validateNonClientHitTestRegion) so both validation paths accept omitted kinds.
In `@webview2/pkg/edge/chromium.go`:
- Around line 331-335: The code currently calls
createCoreWebView2CompositionController() when e.CompositionControllerEnabled is
true and lets any error (including UnsupportedCapabilityError) flow to
errorCallback() and exit; change this to mirror the
NonClientRegionSupportEnabled pattern: when e.CompositionControllerEnabled is
true, call e.createCoreWebView2CompositionController(env) and if it returns
UnsupportedCapabilityError (or errors.Is(..., UnsupportedCapabilityError)) then
fall back to env.CreateCoreWebView2Controller(e.hwnd, e.controllerCompleted)
instead of propagating the unsupported-capability error to errorCallback/exit;
only propagate real failures from CreateCoreWebView2Controller, so
composition-hosting remains optional and non-fatal.
In `@webview2/pkg/edge/com.go`:
- Around line 43-45: The POINT.uintptr method packs X and Y into a single
uintptr but shifts Y by 32 bits which breaks on 32-bit Windows because uintptr
is 32 bits; either restrict this implementation to 64-bit Windows by adding the
build constraint "//go:build windows && amd64" above this file, or change the
implementation to be 32/64-bit safe (for example, return a pair of uintptrs or
use separate calls/fields instead of packing both coords into one uintptr) and
update callers to accept the new representation; locate the method named
POINT.uintptr and apply one of these two fixes so 386 builds do not lose the Y
coordinate.
---
Outside diff comments:
In `@v3/internal/runtime/desktop/`@wailsio/runtime/src/appregion.ts:
- Around line 149-223: The module must only start DOM scanning when the app
explicitly opts-in; update tryStartAppRegionTracking to require a native/JS
opt-in flag (for example check window._wails?.nonClientRegionsEnabled or a new
runtime-provided flag) before calling whenReady(startAppRegionTracking), and
stop polling if the flag is absent so we don’t set up
MutationObserver/resize/scroll handlers unnecessarily; ensure
startAppRegionTracking, scheduleUpdate and updateAppRegions remain unchanged
except that they are only invoked after the opt-in is true, and add a one-time
listener or re-check path so if the opt-in flag appears later the tracking will
start once.
🪄 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: 138a0148-19e4-47fd-af69-7c3ce2cd037a
📒 Files selected for processing (25)
v3/internal/runtime/desktop/@wailsio/runtime/package.jsonv3/internal/runtime/desktop/@wailsio/runtime/src/appregion.tsv3/internal/runtime/desktop/@wailsio/runtime/src/index.tsv3/pkg/application/webview_window.gov3/pkg/application/webview_window_options.gov3/pkg/application/webview_window_windows.gov3/pkg/application/webview_window_windows_nonclient.gov3/pkg/w32/dwmapi.gov3/pkg/w32/user32.gov3/pkg/w32/window.gowebview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.gowebview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.gowebview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.gowebview2/pkg/edge/ICoreWebView2CompositionController.gowebview2/pkg/edge/ICoreWebView2CompositionController4.gowebview2/pkg/edge/ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler.gowebview2/pkg/edge/ICoreWebView2Environment3.gowebview2/pkg/edge/ICoreWebView2Settings9.gowebview2/pkg/edge/IDCompositionDevice.gowebview2/pkg/edge/IDCompositionTarget.gowebview2/pkg/edge/IDCompositionVisual.gowebview2/pkg/edge/chromium.gowebview2/pkg/edge/com.gowebview2/pkg/edge/composition_host.gowebview2/pkg/edge/corewebview2.go
20260516-0054-01.5869215.mp4Demo with Minimize, Maximize and Close buttons. Please note: yellow tips are natively drawn and localized by Windows. PS I don't know why it's so laggy on video, but it works perfectly. |
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 `@webview2/pkg/edge/chromium.go`:
- Around line 330-339: The current branch allows falling back to
env.CreateCoreWebView2Controller when CompositionControllerEnabled is false even
if NonClientRegionSupportEnabled is true, which leaves compositionController4
nil and breaks GetNonClientRegionAtPoint; update the controller initialization
in the block around createCoreWebView2CompositionController (and the condition
that checks e.CompositionControllerEnabled) to either (a) auto-enable
CompositionControllerEnabled and call createCoreWebView2CompositionController if
e.NonClientRegionSupportEnabled is true, or (b) return/reject early with an
error when NonClientRegionSupportEnabled is true but
CompositionControllerEnabled is false; ensure initializeController() will then
populate compositionController4 so GetNonClientRegionAtPoint can return HT*
values for Snap Assist.
🪄 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: 6e2071e5-49c1-4267-8f00-e242dec15c34
📒 Files selected for processing (3)
v3/pkg/application/webview_window.gov3/pkg/application/webview_window_windows.gowebview2/pkg/edge/chromium.go
🚧 Files skipped from review as they are similar to previous changes (2)
- v3/pkg/application/webview_window_windows.go
- v3/pkg/application/webview_window.go
There was a problem hiding this comment.
🧹 Nitpick comments (1)
v3/pkg/application/webview_window_windows_nonclient.go (1)
33-35: 💤 Low valueUnused type definition.
snapLayoutHoverStateis declared but never referenced in this file or the related Windows files.Suggested removal
-type snapLayoutHoverState struct { - tracking bool -} -🤖 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_nonclient.go` around lines 33 - 35, Remove the unused type definition snapLayoutHoverState (type snapLayoutHoverState struct { tracking bool }) since it is not referenced anywhere; delete that type declaration and clean up any now-unused imports or comments that refer to it to keep the Windows nonclient code tidy.
🤖 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.
Nitpick comments:
In `@v3/pkg/application/webview_window_windows_nonclient.go`:
- Around line 33-35: Remove the unused type definition snapLayoutHoverState
(type snapLayoutHoverState struct { tracking bool }) since it is not referenced
anywhere; delete that type declaration and clean up any now-unused imports or
comments that refer to it to keep the Windows nonclient code tidy.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 07ba4797-5c29-4c34-95fd-130e51513f05
📒 Files selected for processing (3)
v3/pkg/application/webview_window.gov3/pkg/application/webview_window_windows.gov3/pkg/application/webview_window_windows_nonclient.go
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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_nonclient.go`:
- Around line 49-53: WM_NCHITTEST currently calls resizeBorderHitTest() before
nonClientHitTestFromScreen(), causing configured caption button regions to be
treated as resize zones; change the hit-test order so
nonClientHitTestFromScreen(screenX, screenY) (which returns HT*BUTTON for custom
caption regions) is evaluated first and if it returns a non-client button hit,
return that immediately; alternatively, modify resizeBorderHitTest(screenX,
screenY) to check for and exempt points that fall inside configured
caption-button regions (reuse the same detection logic used by
nonClientHitTestFromScreen or a shared helper) so caption buttons outrank resize
hit testing (apply the same change for the other occurrence around the block
referenced as lines 168-170).
- Around line 322-338: The WM_NCRBUTTONDOWN/WM_NCRBUTTONUP messages are being
treated as client-area events in routeCompositionMouseInput and
compositionMouseClientPoint, causing screen coordinates and hit-test info in
wParam to be misused (sent via SendMouseInput with
COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS); update routeCompositionMouseInput to
detect WM_NCRBUTTONDOWN and WM_NCRBUTTONUP and handle them separately: either
(A) skip routing these messages so DefWindowProc handles the native non-client
behavior (system menu) or (B) convert the screen coordinates from lParam to
client coordinates before calling SendMouseInput and derive actual modifier keys
via GetKeyState/GetAsyncKeyState rather than using the hit-test in wParam;
modify compositionMouseClientPoint and the call site that invokes SendMouseInput
to implement the chosen approach and ensure WM_NCRBUTTON* are not forwarded as
client-area events.
In `@v3/pkg/application/webview_window_windows.go`:
- Around line 355-356: The code currently sets
w.chromium.NonClientRegionSupportEnabled independently of
w.chromium.CompositionControllerEnabled, allowing
options.Windows.NonClientRegionSupport = true while
options.Windows.WebView2CompositionHosting = false which makes the feature a
silent no-op; fix by adding a check when initializing these flags (where
w.chromium.NonClientRegionSupportEnabled and
w.chromium.CompositionControllerEnabled are set) to either (a) auto-enable
composition hosting: if options.Windows.NonClientRegionSupport &&
!options.Windows.WebView2CompositionHosting then set
w.chromium.CompositionControllerEnabled = true, or (b) fail-fast: return an
error or panic rejecting the unsupported combination (NonClientRegionSupport
true + WebView2CompositionHosting false); implement one of these behaviors in
the same initialization code path so the runtime message routing logic that
depends on composition hosting is always consistent with the flag.
🪄 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: 877c8576-5248-4bc7-8806-41f560e671a8
📒 Files selected for processing (5)
v3/internal/runtime/desktop/@wailsio/runtime/src/drag.tsv3/pkg/application/webview_window.gov3/pkg/application/webview_window_windows.gov3/pkg/application/webview_window_windows_nonclient.gowebview2/pkg/edge/chromium.go
🚧 Files skipped from review as they are similar to previous changes (2)
- v3/pkg/application/webview_window.go
- webview2/pkg/edge/chromium.go
af8f4e3 to
79c4585
Compare
|
@leaanthony I simplified a ton of non-client related logic, reviewing it should be simpler now. |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
Status, support, documentation and community
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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_nonclient.go`:
- Around line 17-20: set() stores the incoming slice directly, so the caller’s
backing array can be mutated after the lock and cause data races; change
nonClientHitTestState.set to make a new slice copy of the provided regions
(e.g., allocate a new slice with the same len and copy elements, or use
append([]nonClientHitTestRegion(nil), regions...)) while holding s.mu, then
assign that copy to s.regions so s.regions does not share the caller’s backing
array; ensure you still lock/unlock s.mu around the allocation+assignment to
avoid races.
In `@v3/pkg/w32/window.go`:
- Around line 67-70: The comment and the MARGINS value disagree: MARGINS{1, 1,
0, 1} sets cxLeftWidth=1, cxRightWidth=1, cyTopHeight=0, cyBottomHeight=1 (so it
extends left/right/bottom, not top). Update either the comment or the value in
the margins assignment in window.go to match intent: if you meant to extend the
top edge change the third field (cyTopHeight) to 1 (e.g., MARGINS{1,1,1,1}),
otherwise correct the comment text near the MARGINS/`margins` declaration to say
it extends the bottom edge (not the top).
In `@webview2/pkg/edge/ICoreWebView2CompositionController.go`:
- Around line 57-59: Replace the strict equality check against windows.S_OK with
the project's HRESULT-to-error semantics: use the existing hresultToError helper
(or the FAILED(hr) check int32(hr) < 0) instead of comparing windows.Handle(hr)
!= windows.S_OK so that non-zero success codes like S_FALSE are treated as
success; update all occurrences in this file (the short-circuit return blocks
that currently check windows.Handle(hr) != windows.S_OK) to call
hresultToError(hr) and return its result when negative, mirroring the pattern
used in chromium.go and corewebview2.go.
🪄 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: a2052870-7ae4-488b-be5f-4501ee707904
📒 Files selected for processing (32)
v3/internal/runtime/desktop/@wailsio/runtime/package.jsonv3/internal/runtime/desktop/@wailsio/runtime/src/appregion.tsv3/internal/runtime/desktop/@wailsio/runtime/src/drag.tsv3/internal/runtime/desktop/@wailsio/runtime/src/index.tsv3/pkg/application/application_server.gov3/pkg/application/webview_window.gov3/pkg/application/webview_window_android.gov3/pkg/application/webview_window_darwin.gov3/pkg/application/webview_window_ios.gov3/pkg/application/webview_window_linux.gov3/pkg/application/webview_window_options.gov3/pkg/application/webview_window_windows.gov3/pkg/application/webview_window_windows_nonclient.gov3/pkg/w32/dwmapi.gov3/pkg/w32/user32.gov3/pkg/w32/window.gowebview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.gowebview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.gowebview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.gowebview2/pkg/edge/ICoreWebView2CompositionController.gowebview2/pkg/edge/ICoreWebView2CompositionController4.gowebview2/pkg/edge/ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler.gowebview2/pkg/edge/ICoreWebView2CursorChangedEventHandler.gowebview2/pkg/edge/ICoreWebView2Environment3.gowebview2/pkg/edge/ICoreWebView2Settings9.gowebview2/pkg/edge/IDCompositionDevice.gowebview2/pkg/edge/IDCompositionTarget.gowebview2/pkg/edge/IDCompositionVisual.gowebview2/pkg/edge/chromium.gowebview2/pkg/edge/com.gowebview2/pkg/edge/composition_host.gowebview2/pkg/edge/corewebview2.go
✅ Files skipped from review due to trivial changes (1)
- webview2/pkg/edge/ICoreWebView2CursorChangedEventHandler.go
🚧 Files skipped from review as they are similar to previous changes (19)
- v3/internal/runtime/desktop/@wailsio/runtime/package.json
- webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_KIND.go
- v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
- v3/pkg/application/webview_window_options.go
- webview2/pkg/edge/COREWEBVIEW2_NON_CLIENT_REGION_KIND.go
- webview2/pkg/edge/COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS.go
- webview2/pkg/edge/IDCompositionVisual.go
- webview2/pkg/edge/composition_host.go
- v3/pkg/w32/dwmapi.go
- v3/pkg/application/webview_window_windows.go
- v3/internal/runtime/desktop/@wailsio/runtime/src/appregion.ts
- v3/pkg/application/webview_window.go
- webview2/pkg/edge/corewebview2.go
- webview2/pkg/edge/IDCompositionTarget.go
- webview2/pkg/edge/ICoreWebView2Environment3.go
- webview2/pkg/edge/ICoreWebView2Settings9.go
- webview2/pkg/edge/IDCompositionDevice.go
- webview2/pkg/edge/ICoreWebView2CompositionController4.go
- webview2/pkg/edge/chromium.go
|
@leaanthony a small ping. I am not in a hurry, just a little afraid that it will be hard to rebase in the future. |
|
Hey 👋 This mostly looks good! It would be good to add a guide for this feature so devs can understand how to use it. Are you ok doing that? You have some great screenshots and videos which could be used 👍 |
|
@leaanthony yes, will do! |

Description
Summary
This PR adds Windows WebView2 non-client region support for custom titlebars and caption buttons.
Changes
ICoreWebView2CompositionController.ICoreWebView2CompositionController4(this one allows you to use nativeapp-regionCSS property which is a better alternative to--wails-draggable). It's not required.--wails-non-client-regionCSS property with four available options:caption | minimize | maximize | close.wails:non-client-region:<json>.Options could be enabled using those properties under
WindowsWindow:NonClientRegionSupportWebView2CompositionHosting.Unfortunately, I wasn't able to use the experimental WCO (Window Controls Overlay) API; in my opinion, it's currently bugged. I see the same behavior in minimal C++ and C# prototypes. Main problem: buttons continue to behave as if they are hovered even when the cursor has left the window. Also, there's a strange undocumented requirement to place an
app-region: dragobject below it to have animations. I also tried to use official Microsoft samples, but they have the same issues and even worse. Seemingly it's in preview for a reason.Fixes #2237
Type of change
Please select the option that is relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration using
wails doctor.If you checked Linux, please specify the distro and version.
Test Configuration
Please paste the output of
wails doctor. If you are unable to run this command, please describe your environment in as much detail as possible.Checklist:
website/src/pages/changelog.mdxwith details of this PR (v3 changelog entries are added automatically)Summary by CodeRabbit
New Features
Bug Fixes
Chores