Skip to content

Feat/macos non activating panel - #5360

Closed
phoenixsheppard28 wants to merge 5 commits into
wailsapp:masterfrom
phoenixsheppard28:feat/macos-non-activating-panel
Closed

phoenixsheppard28 wants to merge 5 commits into
wailsapp:masterfrom
phoenixsheppard28:feat/macos-non-activating-panel

Conversation

@phoenixsheppard28

@phoenixsheppard28 phoenixsheppard28 commented May 7, 2026 •

Copy link
Copy Markdown

Description

This PR adds macOS support for non-activating panels (Swift’s nonactivatingPanel–style behaviour): optional NSWindowStyleMaskNonactivatingPanel on Wails webview windows so users can build floating palettes, Spotlight-style overlays, and menu-bar detail windows without stealing activation from the app that currently has focus.

Summary of changes:

  • MacWindow.NonActivatingPanel (webview_window_options.go) — opt-in flag with GoDoc that separates application Options.Mac / MacOptions.ActivationPolicy from per-window WebviewWindowOptions.Mac (MacWindow) settings.
  • WebviewWindow subclasses NSPanel (webview_window_darwin.h) so the non-activating style mask is honoured by AppKit.
  • windowNew ORs NSWindowStyleMaskNonactivatingPanel when the option is set; canBecomeMainWindow returns NO when that mask is present (panel stays key-capable for text input but not main).
  • windowFocus skips activateIgnoringOtherApps: when the non-activating mask is set so “focus window” does not force app activation.
  • NSPanel defaults — hidesOnDeactivate forced to NO so windows do not disappear on app switch; releasedWhenClosed stays NO by design.
  • Lifetime — With releasedWhenClosed == NO, windowClose and windowDestroy now [release] after [close] to balance windowNew’s alloc; Go nil-s nsWindow after teardown to avoid dangling pointers.
  • Manual test — v3/test/manual/macos/non-activating-panel/ plus v3/test/manual/macos/README.md with a reproducible checklist.

Pairings users will typically use (documented on the field): Mac.WindowLevel, Mac.CollectionBehavior, Frameless/Mac.Backdrop; dock-hidden / accessory behaviour remains Options.Mac.ActivationPolicy (MacOptions).

Fixes #5359

Type of change

Please select the option that is relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

(Bug-fix aspects: NSPanel hidesOnDeactivate regression, ObjC window lifetime / release after close when releasedWhenClosed is NO.)

How Has This Been Tested?

  • Windows (not applicable — macOS-only paths)
  • macOS
  • Linux

macOS

  • go build ./pkg/application/ and go vet ./pkg/application/ from v3/.
  • Manual: cd v3/test/manual/macos/non-activating-panel && go run . — follow v3/test/manual/macos/README.md (non-activating vs normal control window, hidesOnDeactivate, focus/activation, canBecomeMainWindow, text input, close/re-run).

Test Configuration

# System

┌──────────────────────────────────────────────────┐
| Name          | MacOS                            |
| Version       | 15.6.1                           |
| ID            | 24G90                            |
| Branding      | Sequoia                          |
| Platform      | darwin                           |
| Architecture  | arm64                            |
| Apple Silicon | true                             |
| CPU           | Apple M4 Pro                     |
| CPU 1         | Apple M4 Pro                     |
| CPU 2         | Apple M4 Pro                     |
| GPU           | 16 cores, Metal Support: Metal 3 |
| Memory        | 24 GB                            |
└──────────────────────────────────────────────────┘

# Build Environment

┌──────────────────────────────────────────────────────────────────────┐
| Wails CLI      | v3.0.0-alpha.85                                     |
| Go Version     | go1.26.1                                            |
| -buildmode     | exe                                                 |
| -compiler      | gc                                                  |
| CGO_CFLAGS     |                                                     |
| CGO_CPPFLAGS   |                                                     |
| CGO_CXXFLAGS   |                                                     |
| CGO_ENABLED    | 1                                                   |
| CGO_LDFLAGS    |                                                     |
| DefaultGODEBUG | cryptocustomrand=1,tlssecpmlkem=0,urlstrictcolons=0 |
| GOARCH         | arm64                                               |
| GOARM64        | v8.0                                                |
| GOOS           | darwin                                              |
└──────────────────────────────────────────────────────────────────────┘

# Dependencies

┌──────────────────────────────────────────────────────────────────────────────┐
| *NSIS           | Not Installed. Install with `brew install makensis`.       |
| Xcode cli tools | 2410                                                       |
| npm             | 11.7.0                                                     |
| docker          | *Docker version 28.5.2, build ecc6942 (daemon not running) |
|                                                                              |
└────────────────────────── * - Optional Dependency ───────────────────────────┘

# Checking for issues

 SUCCESS  No issues found

# Diagnosis

 SUCCESS  Your system is ready for Wails development!

Checklist:

  • (v2 only) I have updated website/src/pages/changelog.mdx with details of this PR (v3 changelog entries are added automatically)
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Added a macOS NonActivatingPanel option to create panels that do not steal focus but can still accept input.
  • Improvements

    • Improved macOS window lifecycle handling for greater stability and safer window closing behavior.
  • Documentation

    • Added docs describing the NonActivatingPanel option and usage examples.
  • Tests

    • Added a manual macOS test and test guide for verifying panel and window activation behaviors.

@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a macOS NonActivatingPanel option: WebviewWindow becomes an NSPanel, lifecycle functions explicitly release Objective‑C objects and clear Go pointers, the NonActivatingPanel flag is wired through creation, and manual tests and docs are added.

Changes

macOS NonActivatingPanel Window Feature

Layer / File(s) Summary
Public API
v3/pkg/application/webview_window_options.go
Adds NonActivatingPanel bool to MacWindow with docs describing NSPanel/non-activating behavior.
Objective-C Base Class
v3/pkg/application/webview_window_darwin.h, v3/pkg/application/webview_window_darwin.m
WebviewWindow now subclasses NSPanel; releasedWhenClosed disabled; hidesOnDeactivate set; canBecomeKeyWindow returns YES; canBecomeMainWindow returns NO for non-activating panels.
Object Lifecycle
v3/pkg/application/webview_window_darwin.go
C windowDestroy/windowClose release the Objective‑C object; Go close()/destroy() guard nil and set nsWindow = nil after calling C.
Feature Wiring
v3/pkg/application/webview_window_darwin.go
windowNew applies NSWindowStyleMaskNonactivatingPanel when requested; run() passes macOptions.NonActivatingPanel to C constructor.
Tests / Docs
v3/test/manual/macos/*, docs/src/content/docs/features/windows/*.mdx
Manual macOS test program and README for non-activating panel; docs updated with usage example and option description.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wailsapp/wails#4323: Modifies macOS window lifecycle and native bridge in webview_window_darwin files, touching close/destroy behavior.
  • wailsapp/wails#5307: Modifies macOS WebviewWindow implementation and mac-specific options in related darwin files.
  • wailsapp/wails#4827: Adds tests validating MacWindow/webview option defaults (potentially related to option additions).

Suggested labels

MacOS, Enhancement, Documentation, v3, size:M

Suggested reviewers

  • leaanthony

Poem

🐰 A little panel, soft and light,
It sits above, but keeps things right.
Type away without a fight,
The other app stays in the light.
Memory tidy, behavior bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/macos non activating panel' clearly and specifically summarizes the main change—adding macOS non-activating panel support.
Linked Issues check ✅ Passed The PR fully implements the requirements from #5359: adds NonActivatingPanel bool to MacWindow, subclasses WebviewWindow from NSPanel, handles style mask and focus behavior, manages window lifecycle, and includes manual tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the non-activating panel feature: macOS window implementation, window options, manual testing, and user-facing documentation. No unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description comprehensively covers all required template sections including a detailed summary, issue reference, type of change checkboxes, testing approach with macOS validation, test configuration output, and completed checklist items.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v3/pkg/application/webview_window_darwin.go (1)

1630-1638: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

destroy() is missing the unconditionallyClose flag, risking [w release] on an AppKit-open window.

close() sets atomic.StoreUint32(&w.parent.unconditionallyClose, 1) before calling C.windowClose, so the delegate's windowShouldClose: returns YES and [w close] completes cleanly. destroy() makes no such guarantee. When called without a prior close() (e.g., Window.Destroy(), or app-quit where windows were never individually closed), C.windowDestroy calls [w close] on an unconditional-flag-unset window: the delegate fires EventWindowShouldClose and returns NO, abandoning the close sequence. [w release] then executes unconditionally, freeing the Obj-C object while AppKit still considers the window open — a use-after-free.

🐛 Proposed fix
 func (w *macosWebviewWindow) destroy() {
 	w.parent.markAsDestroyed()
 	clearWindowDragCache(w.parent.id)
 	if w.nsWindow != nil {
+		// Mirror close(): ensure windowShouldClose: returns YES so [w close]
+		// completes the close sequence before [w release] in windowDestroy.
+		atomic.StoreUint32(&w.parent.unconditionallyClose, 1)
 		C.windowDestroy(w.nsWindow)
 		w.nsWindow = nil
 	}
 }
🤖 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_darwin.go` around lines 1630 - 1638, The
destroy() path can call C.windowDestroy which may trigger Obj-C [w close] while
the parent.unconditionallyClose flag is not set, causing the delegate to cancel
the close and later release the window leading to use-after-free; modify
macosWebviewWindow.destroy to set
atomic.StoreUint32(&w.parent.unconditionallyClose, 1) (same flag used in
close()) before invoking C.windowDestroy so the delegate's windowShouldClose:
returns YES and the close/destroy sequence completes safely; reference
functions/fields: macosWebviewWindow.destroy, macosWebviewWindow.close,
parent.unconditionallyClose, C.windowDestroy, C.windowClose, and the
windowShouldClose/EventWindowShouldClose delegate behavior to mirror close()'s
ordering.
🧹 Nitpick comments (1)
v3/pkg/application/webview_window_darwin.go (1)

706-731: 💤 Low value

windowDestroy and static windowClose are now functionally identical.

Both functions execute [w close]; [w release]. If the implementations are intended to remain in sync, consider factoring the shared body into an inline helper or adding a comment linking them. This prevents a future divergence where one is updated but the other isn't.

🤖 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_darwin.go` around lines 706 - 731,
windowDestroy and the static function windowClose both perform identical actions
([w close]; [w release]) on a WebviewWindow, risking divergence; refactor by
extracting the shared behavior into a single helper (e.g., windowCloseHelper or
inline function) and have both windowDestroy and windowClose call that helper,
or implement windowClose to simply call windowDestroy with the same
WebviewWindow* parameter to keep them in sync (refer to windowDestroy,
windowClose, and WebviewWindow).
🤖 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.

Outside diff comments:
In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 1630-1638: The destroy() path can call C.windowDestroy which may
trigger Obj-C [w close] while the parent.unconditionallyClose flag is not set,
causing the delegate to cancel the close and later release the window leading to
use-after-free; modify macosWebviewWindow.destroy to set
atomic.StoreUint32(&w.parent.unconditionallyClose, 1) (same flag used in
close()) before invoking C.windowDestroy so the delegate's windowShouldClose:
returns YES and the close/destroy sequence completes safely; reference
functions/fields: macosWebviewWindow.destroy, macosWebviewWindow.close,
parent.unconditionallyClose, C.windowDestroy, C.windowClose, and the
windowShouldClose/EventWindowShouldClose delegate behavior to mirror close()'s
ordering.

---

Nitpick comments:
In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 706-731: windowDestroy and the static function windowClose both
perform identical actions ([w close]; [w release]) on a WebviewWindow, risking
divergence; refactor by extracting the shared behavior into a single helper
(e.g., windowCloseHelper or inline function) and have both windowDestroy and
windowClose call that helper, or implement windowClose to simply call
windowDestroy with the same WebviewWindow* parameter to keep them in sync (refer
to windowDestroy, windowClose, and WebviewWindow).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 872406b9-c101-4a4d-bd09-bc8b288e44de

📥 Commits

Reviewing files that changed from the base of the PR and between 3294a6b and 315711b.

📒 Files selected for processing (6)
  • v3/pkg/application/webview_window_darwin.go
  • v3/pkg/application/webview_window_darwin.h
  • v3/pkg/application/webview_window_darwin.m
  • v3/pkg/application/webview_window_options.go
  • v3/test/manual/macos/README.md
  • v3/test/manual/macos/non-activating-panel/main.go

@leaanthony

Copy link
Copy Markdown
Member

This is a significant new feature adding macOS non-activating panel support. The implementation looks comprehensive with good documentation and manual tests, but I'd like a maintainer to review the NSPanel integration and window lifecycle changes before proceeding.

CC @leaanthony

@leaanthony

Copy link
Copy Markdown
Member

Thank you for the detailed implementation and test plan, @phoenixsheppard28. Your analysis of non-activating Show/Focus behavior, deactivation defaults, close/recreate lifetime, and the need for an NSWindow-versus-NSPanel comparison helped establish the acceptance criteria for this feature.

We have consolidated the work in #6008. It keeps existing windows backed by NSWindow and makes NSPanel an explicit per-window class, while sharing the current WebKit, keybinding, drag-and-drop, and lifecycle paths. It also includes panel preferences, documentation, examples, and dedicated activation/input/lifecycle testing.

To avoid maintaining two overlapping implementations, I am closing this PR as superseded by #6008. Your contribution is credited there, and a review of the successor would be very welcome. Thank you for moving the non-activating-panel behavior and lifecycle testing forward.

@leaanthony leaanthony closed this Aug 21, 2026
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