Skip to content

feat(darwin): Add NSPanel support for Spotlight-like windows#5024

Open
Grantmartin2002 wants to merge 10 commits intowailsapp:v3-alphafrom
Grantmartin2002:v3-alpha-feature/issues-5023
Open

feat(darwin): Add NSPanel support for Spotlight-like windows#5024
Grantmartin2002 wants to merge 10 commits intowailsapp:v3-alphafrom
Grantmartin2002:v3-alpha-feature/issues-5023

Conversation

@Grantmartin2002
Copy link

@Grantmartin2002 Grantmartin2002 commented Feb 27, 2026

This adds support for creating NSPanel-based windows instead of NSWindow, enabling auxiliary windows that can appear over fullscreen apps without activating the application.

Key changes

  • Add WindowClass MacWindowClass to MacWindow configuration with NSWindow (default) and NSPanel options
  • Add MacPanelOptions struct with panel-specific options:
    • FloatingPanel - float above other windows
    • BecomesKeyOnlyIfNeeded - become key only when needed
    • NonactivatingPanel - receive input without activating the app
    • UtilityWindow - utility window style
  • Implement WebviewPanel class (NSPanel subclass) with same functionality as WebviewWindow
  • Add panelNew() function to create panels with configurable style masks
  • Override sendEvent: on both WebviewWindow and WebviewPanel to ensure key bindings work regardless of WKWebView first responder state. My keybindings no longer worked when I used a panel until I did this. Side effect free??? maybe add an option for this?

The NSWindowStyleMaskNonactivatingPanel style allows the panel to receive keyboard input without activating the owning application.

Feedback wanted:

  • Send event override side effect free? Should make an option instead? Different approach entirely?
  • Duplicating code in nspanel and nswindow. Should add helpers in the objective c? Not sure how you'd like to structure it.

Fixes #5023

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

How Has This Been Tested?

  • Windows
  • macOS
  • Linux
┌──────────────────────────────────────────────────┐
| Name          | MacOS                            |
| Version       | 15.7.1                           |
| ID            | 24G231                           |
| Branding      | Sequoia                          |
| Platform      | darwin                           |
| Architecture  | arm64                            |
| Apple Silicon | true                             |
| CPU           | Apple M4 Pro                     |
| CPU 1         | Apple M4 Pro                     |
| CPU 2         | Apple M4 Pro                     |
| GPU           | 20 cores, Metal Support: Metal 3 |
| Memory        | 48 GB                            |
└──────────────────────────────────────────────────┘

# Build Environment 

┌────────────────────────────────┐
| Wails CLI    | v3.0.0-alpha.71 |
| Go Version   | go1.25.1        |
| -buildmode   | exe             |
| -compiler    | gc              |
| CGO_CFLAGS   |                 |
| CGO_CPPFLAGS |                 |
| CGO_CXXFLAGS |                 |
| CGO_ENABLED  | 1               |
| CGO_LDFLAGS  |                 |
| GOARCH       | arm64           |
| GOARM64      | v8.0            |
| GOOS         | darwin          |
└────────────────────────────────┘

# Dependencies 

┌───────────────────────────────────────────────────────────────────────────────────┐
| Xcode cli tools | 2410                                                            |
| npm             | 11.5.1                                                          |
| *NSIS           | v3.11                                                           |
| docker          | *Docker version 28.2.2, build e6534b4 (cross-compilation ready) |
|                                                                                   |
└───────────────────────────── * - Optional Dependency ─────────────────────────────┘

# Checking for issues 

 SUCCESS  No issues found

# Diagnosis 

 SUCCESS  Your system is ready for Wails development!

Need documentation? Run: wails3 docs
 ♥   If Wails is useful to you or your company, please consider sponsoring the project: wails3 sponsor

Checklist:

  • I have updated website/src/pages/changelog.mdx with details of this PR
  • 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 macOS NSPanel support as an alternative to standard windows, selectable per window.
    • Introduced configurable panel options (floating, becomes-key-only-if-needed, non-activating, utility) for finer macOS panel behavior.
    • More consistent keyboard event handling across window and panel types.
  • Documentation

    • Changelog and docs updated with NSPanel options, examples (including a Spotlight-like panel), and usage guidance.

grantmartin2002-oss and others added 3 commits February 27, 2026 15:27
This adds support for creating NSPanel-based windows instead of NSWindow,
enabling Spotlight-like auxiliary windows that can appear over fullscreen
apps without yanking the user out of fullscreen.

Key changes:
- Add UsePanel and PanelOptions to MacWindow configuration
- Implement WebviewPanel class (NSPanel subclass) with same functionality as WebviewWindow
- Add panelNew() function to create panels with NonactivatingPanel style
- Override sendEvent: on both WebviewWindow and WebviewPanel to ensure
  KeyBindings work regardless of WKWebView first responder state
- Add collection behavior and window level configuration options

The NSPanel with NSWindowStyleMaskNonactivatingPanel allows the panel to
receive keyboard input without activating the owning application, which
is the key feature for Spotlight/Alfred-like interfaces.

Fixes wailsapp#5023

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Walkthrough

Adds NSPanel support on macOS: introduces a WebviewPanel (NSPanel subclass) and C API panelNew, updates Go window creation to choose NSPanel when WindowClass==NSPanel, and exposes Mac.WindowClass and Mac.PanelOptions for panel configuration and keyboard handling.

Changes

Cohort / File(s) Summary
Core Panel Implementation
v3/pkg/application/webview_window_darwin.h, v3/pkg/application/webview_window_darwin.m
Adds WebviewPanel (subclass of NSPanel) mirroring WebviewWindow: init, sendEvent/keyDown interception, keyStringFromEvent, responder methods, delegate handling, drag registration, and dealloc cleanup.
Panel Creation Logic
v3/pkg/application/.../webview_window_darwin.go
Adds exported C API panelNew(...) to construct/configure NSPanel instances (styleMask, frameless, drag-and-drop, WKWebView prefs) and branches Go-side window creation to call panelNew when WindowClass == NSPanel.
Options / API Surface
v3/pkg/application/webview_window_options.go
Adds MacWindowClass enum (NSWindow, NSPanel) and MacPanelOptions struct (FloatingPanel, BecomesKeyOnlyIfNeeded, NonactivatingPanel, UtilityWindow) on MacWindow.
Docs / Changelog
website/src/pages/changelog.mdx, docs/src/content/docs/features/windows/options.mdx
Documents NSPanel support, adds examples (spotlight-like panel), and documents new WindowClass and PanelOptions usage. Minor formatting/comment tweaks elsewhere.

Sequence Diagram(s)

sequenceDiagram
    participant App as Go App
    participant Router as Window Creation Router
    participant PanelC as panelNew (C)
    participant Panel as WebviewPanel
    participant Delegate as Window Delegate
    participant WebKit as WKWebView

    App->>Router: Request window with WindowClass=NSPanel
    Router->>PanelC: Call panelNew with options
    PanelC->>Panel: Instantiate WebviewPanel with styleMask & panel flags
    PanelC->>WebKit: Create WKWebView with WebviewPreferences
    Panel->>Panel: Embed WKWebView in content view
    PanelC->>Delegate: Set delegate and register drag types
    PanelC->>App: Return panel instance

    Note over Panel: Keyboard Event Flow
    App->>Panel: User keydown
    Panel->>Panel: sendEvent -> intercept keydown
    Panel->>Panel: keyDown -> keyStringFromEvent
    Panel->>Delegate: processWindowKeyDownEvent(modifier+key)
    Delegate->>App: Route to handler
Loading
sequenceDiagram
    participant Comparison as Creation Paths

    rect rgba(100,150,200,0.5)
        Note over Comparison: windowNew (NSWindow)
        participant WindowC as windowNew (C)
        participant Window as WebviewWindow
        WindowC->>Window: Create standard NSWindow, set WKWebView, delegate
    end

    rect rgba(150,100,200,0.5)
        Note over Comparison: panelNew (NSPanel) — NEW
        participant PanelC as panelNew (C)
        participant Panel as WebviewPanel
        PanelC->>Panel: Create NSPanel with panel-specific flags, set WKWebView, delegate
    end

    Note over Comparison: Both converge on delegate, WKWebView setup, and keyboard interception
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Enhancement, MacOS, v3-alpha, Documentation, size:XL, lgtm

Suggested reviewers

  • leaanthony
  • atterpac

Poem

🐰 I hop in code with a tiny panel bright,
Floating above full-screen, soft as moonlight.
Keys tap and events prance through my fur,
Spotlight glows — I twirl in a blur.
A tiny rabbit cheers: panels take flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(darwin): Add NSPanel support for Spotlight-like windows' clearly and concisely summarizes the main change: adding NSPanel support for macOS windows.
Description check ✅ Passed The PR description includes a summary of changes, links to the related issue (#5023), specifies the change type (New feature), provides detailed test configuration, and checks the changelog update requirement.
Linked Issues check ✅ Passed The PR fully addresses issue #5023 by implementing NSPanel support with NSWindowStyleMaskNonactivatingPanel style, allowing panels to receive input without activating the application.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing NSPanel support for macOS windows as requested in #5023; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@Grantmartin2002 Grantmartin2002 force-pushed the v3-alpha-feature/issues-5023 branch from d8ef125 to ffe6e91 Compare February 28, 2026 00:18
@Grantmartin2002 Grantmartin2002 marked this pull request as ready for review February 28, 2026 00:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
v3/pkg/application/webview_window_darwin.m (1)

279-411: Extract shared key mapping/serialization for window + panel.

WebviewPanel and WebviewWindow now duplicate the full key parsing map. Consider a shared helper to avoid future divergence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.m` around lines 279 - 411, Both
WebviewPanel and WebviewWindow duplicate the full key parsing/map logic; extract
the mapping and serialization into a shared helper to prevent divergence. Create
a shared utility (e.g., KeyMapper or WebviewKeyHelper) that exports a method
like keyStringFromEvent:(NSEvent*)event and a serializer that builds the
"modifier+key" string (used by keyDown:); replace the local implementations in
WebviewWindow (keyStringFromEvent and keyDown:) and the analogous methods in
WebviewPanel to call the shared helper, keeping existing method names (keyDown:,
keyStringFromEvent:) as thin wrappers so callers/delegates (e.g.,
WebviewWindowDelegate, processWindowKeyDownEvent) continue to work unchanged.
v3/pkg/application/webview_window_darwin.go (1)

183-289: panelNew() and windowNew() are now heavily duplicated.

This is likely to drift over time (preferences, delegates, drag-drop, and webview setup). Consider extracting a shared builder/helper for common setup and keep only style-mask/class-specific bits separate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.go` around lines 183 - 289,
panelNew() and windowNew() contain nearly identical webview/delegate/preferences
setup (WebviewWindowDelegate, WKWebViewConfiguration config,
WKUserContentController, WKWebView webView, drag/drop wiring), so extract the
common logic into a shared helper (eg. configureWebviewContent or
buildWebviewForContainer) that accepts the container view/panel/window, id,
WebviewPreferences, fraudulentWebsiteWarningEnabled, enableDragAndDrop and
returns or assigns the configured WKWebView and delegate; leave only the style
mask and panel-specific bits (frameless, floatingPanel, WebviewPanel creation)
in panelNew(), and call the new helper to set preferences, set URL scheme
handler, userContentController setup, assign navigation/UIDelegate, and attach
drag view when requested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 202-203: The panel's floating state set in panelNew() via [panel
setFloatingPanel:YES] is being overridden later by an unconditional [window
setLevel:MacWindowLevelNormal] call; update the logic that sets default window
level (the code paths around setWindowLevel / setLevel and MacWindowLevelNormal)
to skip applying a default level when WindowClass == NSPanel and
PanelOptions.FloatingPanel is true (or alternatively detect when
setFloatingPanel was explicitly set and avoid calling setLevel), ensuring
panelNew's setFloatingPanel remains effective; look for functions/methods named
panelNew, setFloatingPanel, setWindowLevel/setLevel, and references to
PanelOptions.FloatingPanel, WindowClass/NSPanel and MacWindowLevelNormal to
apply the conditional.

In `@v3/pkg/application/webview_window_darwin.m`:
- Around line 33-41: sendEvent: is causing duplicate key handling by
unconditionally invoking [self keyDown:event] before [super sendEvent:event];
update the sendEvent: implementations in WebviewWindow and WebviewPanel to only
call [self keyDown:event] when the window's firstResponder is not the WKWebView
(or a subview of it) — i.e., obtain the firstResponder from the window and skip
the manual keyDown dispatch if it isKindOfClass: WKWebView (or resides within
the WKWebView), otherwise call [self keyDown:event] and then [super
sendEvent:event] as before.

In `@website/src/pages/changelog.mdx`:
- Line 18: Fix the spelling typo in the changelog entry: replace the misspelled
word "auxillary" with "auxiliary" in the sentence describing NSPanel support
(the line mentioning "Panels serve as auxillary windows..."); ensure the rest of
the text (references to `NSWindowStyleMaskNonactivatingPanel`,
`Mac.WindowClass`, and `Mac.PanelOptions`) remains unchanged.

---

Nitpick comments:
In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 183-289: panelNew() and windowNew() contain nearly identical
webview/delegate/preferences setup (WebviewWindowDelegate,
WKWebViewConfiguration config, WKUserContentController, WKWebView webView,
drag/drop wiring), so extract the common logic into a shared helper (eg.
configureWebviewContent or buildWebviewForContainer) that accepts the container
view/panel/window, id, WebviewPreferences, fraudulentWebsiteWarningEnabled,
enableDragAndDrop and returns or assigns the configured WKWebView and delegate;
leave only the style mask and panel-specific bits (frameless, floatingPanel,
WebviewPanel creation) in panelNew(), and call the new helper to set
preferences, set URL scheme handler, userContentController setup, assign
navigation/UIDelegate, and attach drag view when requested.

In `@v3/pkg/application/webview_window_darwin.m`:
- Around line 279-411: Both WebviewPanel and WebviewWindow duplicate the full
key parsing/map logic; extract the mapping and serialization into a shared
helper to prevent divergence. Create a shared utility (e.g., KeyMapper or
WebviewKeyHelper) that exports a method like keyStringFromEvent:(NSEvent*)event
and a serializer that builds the "modifier+key" string (used by keyDown:);
replace the local implementations in WebviewWindow (keyStringFromEvent and
keyDown:) and the analogous methods in WebviewPanel to call the shared helper,
keeping existing method names (keyDown:, keyStringFromEvent:) as thin wrappers
so callers/delegates (e.g., WebviewWindowDelegate, processWindowKeyDownEvent)
continue to work unchanged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 972bb6f and cc5af72.

📒 Files selected for processing (5)
  • 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
  • website/src/pages/changelog.mdx

Comment on lines +202 to +203
[panel setFloatingPanel:floatingPanel];
[panel setBecomesKeyOnlyIfNeeded:becomesKeyOnlyIfNeeded];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the exact code at the lines mentioned in the review
cd /tmp/repo 2>/dev/null || cd . 

# Check the file exists and view the key sections
if [ -f "v3/pkg/application/webview_window_darwin.go" ]; then
  echo "=== Lines 195-210 (around panelNew setup) ==="
  sed -n '195,210p' "v3/pkg/application/webview_window_darwin.go"
  
  echo -e "\n=== Lines 1370-1405 (NSPanel branching logic) ==="
  sed -n '1370,1405p' "v3/pkg/application/webview_window_darwin.go"
  
  echo -e "\n=== Lines 1420-1440 (default WindowLevel logic) ==="
  sed -n '1420,1440p' "v3/pkg/application/webview_window_darwin.go"
else
  echo "File not found, listing directory structure..."
  find . -name "webview_window_darwin.go" -type f
fi

Repository: wailsapp/wails

Length of output: 2532


🌐 Web query:

AppKit NSPanel setFloatingPanel behavior when combined with setLevel NSNormalWindowLevel

💡 Result:

-[NSPanel setFloatingPanel:] is effectively a window-level toggle: a panel is considered “floating” when its level is NSFloatingWindowLevel rather than NSNormalWindowLevel (and -isFloatingPanel reflects that) [1]. So if you do:

  • panel.setFloatingPanel(true) → AppKit treats it as floating by setting/keeping its level at NSFloatingWindowLevel [1][2]
  • then panel.setLevel(NSNormalWindowLevel) → you’ve overridden the floating behavior; the panel will no longer be “floating” in the sense that it won’t sit above normal-level windows (and isFloatingPanel would no longer match “floating” semantics) [1]

In other words, combining them is not additive—level “wins” for z-order, and “floating panel” is primarily shorthand for using the floating window level rather than an independent mode [1][2]. If you later call setFloatingPanel(true) again, expect it to push the panel back to the floating level again [1][2].

Sources: [1] NSPanel docs describing floating as “window level is NSFloatingWindowLevel instead of NSNormalWindowLevel” (GNUstep) [2] NSPanel overview noting panels “can be made to float above all other windows” (O’Reilly Cocoa in a Nutshell)


FloatingPanel is unintentionally overridden by the default window level fallback.

When WindowClass == NSPanel and only PanelOptions.FloatingPanel = true is set, the floating behavior is lost because panelNew() applies setFloatingPanel:true (line 202), but the subsequent default logic at line 1428 calls setWindowLevel(MacWindowLevelNormal), which overrides the floating level. In AppKit, an explicit setLevel() call supersedes setFloatingPanel(), causing the panel to no longer float above other windows.

Users who omit WindowLevel when using FloatingPanel = true will get a non-floating panel, which violates the expected behavior.

Suggested fix
if macOptions.WindowLevel == "" {
-	macOptions.WindowLevel = MacWindowLevelNormal
+	if macOptions.WindowClass == NSPanel && macOptions.PanelOptions.FloatingPanel {
+		macOptions.WindowLevel = MacWindowLevelFloating
+	} else {
+		macOptions.WindowLevel = MacWindowLevelNormal
+	}
}

Also applies to: 1375-1399

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.go` around lines 202 - 203, The
panel's floating state set in panelNew() via [panel setFloatingPanel:YES] is
being overridden later by an unconditional [window
setLevel:MacWindowLevelNormal] call; update the logic that sets default window
level (the code paths around setWindowLevel / setLevel and MacWindowLevelNormal)
to skip applying a default level when WindowClass == NSPanel and
PanelOptions.FloatingPanel is true (or alternatively detect when
setFloatingPanel was explicitly set and avoid calling setLevel), ensuring
panelNew's setFloatingPanel remains effective; look for functions/methods named
panelNew, setFloatingPanel, setWindowLevel/setLevel, and references to
PanelOptions.FloatingPanel, WindowClass/NSPanel and MacWindowLevelNormal to
apply the conditional.

Comment on lines +33 to +41
- (void)sendEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown) {
// Process through keybinding system first
[self keyDown:event];
// Still pass to WKWebView for normal input handling
// (keybinding callbacks like Hide will take effect regardless)
}
[super sendEvent:event];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "webview_window_darwin.m" -type f

Repository: wailsapp/wails

Length of output: 102


🏁 Script executed:

cat -n ./v3/pkg/application/webview_window_darwin.m | head -100

Repository: wailsapp/wails

Length of output: 4612


🏁 Script executed:

cat -n ./v3/pkg/application/webview_window_darwin.m | sed -n '260,290p'

Repository: wailsapp/wails

Length of output: 1505


🌐 Web query:

NSWindow sendEvent keyDown double dispatch macOS Cocoa responder chain

💡 Result:

In AppKit, a physical key press can traverse two stages before your view/controller sees it, which is why people often observe “double dispatch”:

  1. NSApplication pre-processing (key equivalents / commands)
    Before the event is delivered as a normal key event, NSApp first tries to treat it as a key equivalent (menu shortcuts, control-key handling, etc.) via performKeyEquivalent: and related routing. If it’s not recognized, it may then fall through and be delivered as a normal NSKeyDown to the window/first responder. Apple explicitly documents this behavior (notably since 10.5) and that some events can be tried as key equivalents first and then still sent as NSKeyDown. [1]

  2. Normal key event dispatch (window → first responder → responder chain)
    For “most key events,” the key window receives sendEvent:, and the window dispatches the event to the first responder as keyDown:; if unhandled it continues up the responder chain. [0] [2]

The most common “double delivery” pitfalls

  • Overriding NSWindow.sendEvent: and manually calling keyDown: (or forwarding) and then also calling super.sendEvent:
    super.sendEvent: will also dispatch the key event to the first responder, so your manual dispatch + super yields two deliveries. (Fix: only dispatch yourself or call super, not both; if you fully handle it, return without calling super.)

  • Seeing both performKeyEquivalent: and keyDown: for the same physical key press
    This is expected for certain key combinations: AppKit may attempt key-equivalent handling first, then (if not handled) deliver it as NSKeyDown through the responder chain. [1]

  • Event monitors stacking up (local monitors added multiple times)
    If you add a local monitor repeatedly and don’t remove it, you can observe repeated handling for one press (each monitor runs). Removing the monitor stops the duplication. [6]

Practical guidance

  • If your goal is “global within the window” handling, prefer overriding keyDown: in the actual first responder (often the view), and let the responder chain work. [0] [2]
  • If your goal is intercepting menu shortcuts / key equivalents, implement/inspect performKeyEquivalent: (usually on views), and return YES only when you truly handled it; returning NO allows further routing. [1]
  • Avoid overriding NSWindow.performKeyEquivalent: (Apple discourages it). [1]

🏁 Script executed:

grep -n "canBecomeKeyWindow\|acceptsFirstResponder" ./v3/pkg/application/webview_window_darwin.m

Repository: wailsapp/wails

Length of output: 197


🏁 Script executed:

cat -n ./v3/pkg/application/webview_window_darwin.m | sed -n '190,200p;412,422p'

Repository: wailsapp/wails

Length of output: 646


🌐 Web query:

WKWebView first responder macOS NSWindow sendEvent keyDown dispatch

💡 Result:

On macOS, keyboard events do not go to the view under the mouse; they go to the first responder of the key window:

  • NSApplication.sendEvent(_:) first checks for key equivalents (e.g. many ⌘ shortcuts) via performKeyEquivalent: routing; if not handled that way, it dispatches the event to the key window, and the window delivers it to its first responder via keyDown:. [1]
  • If your WKWebView (or something inside it) is not actually the window’s first responder, it won’t receive keyDown:—this is the classic “mouseDown works but keyDown doesn’t” issue. [2]

Practical implications for WKWebView

  1. Make sure the web view (or the correct subview) is first responder

    • Ensure the window is key, then call:
      • window.makeFirstResponder(webView) (or the specific subview that should type)
    • A view must accept first responder; many views won’t unless acceptsFirstResponder returns true. [2]
  2. If you’re overriding NSWindow.sendEvent(_:)

    • If you intercept .keyDown and don’t call super.sendEvent(event), you can break the normal dispatch path to the first responder. The “default” path is: app → window → first responder. [1]
  3. Why some ⌘ shortcuts may not arrive as keyDown:

    • They can be handled earlier as key equivalents (performKeyEquivalent:) before the window sends keyDown: to the responder chain. [1]
  4. If you need to observe/intercept keyDown before it reaches WKWebView

    • Use a local event monitor (NSEvent.addLocalMonitorForEvents) to see events as your app dispatches them; unlike a global monitor, it’s within your app’s event stream. [3]

Sources: Apple Cocoa Event Handling Guide on key-event routing and key equivalents [1], responder/first-responder behavior explanation [2], Apple docs on event monitors [3].


Double keybinding dispatch risk: sendEvent: calls both window and responder keyDown: for same event.

Manually calling [self keyDown:event] before [super sendEvent:event] causes the key event to be processed twice: once by the window's override, then again by the first responder (likely WKWebView) when super dispatches it. This violates Cocoa's event dispatch pattern and can cause unintended duplicate handling.

This applies to both the WebviewWindow (lines 33–41) and WebviewPanel (lines 270–278) classes.

Suggested fix
- (void)sendEvent:(NSEvent *)event {
    if (event.type == NSEventTypeKeyDown) {
-       [self keyDown:event];
+       id responder = self.firstResponder;
+       BOOL responderInsideWebView = NO;
+       if ([responder isKindOfClass:[NSView class]] && self.webView != nil) {
+           responderInsideWebView = [((NSView *)responder) isDescendantOf:self.webView];
+       }
+       if (!responderInsideWebView) {
+           [self keyDown:event];
+       }
    }
    [super sendEvent:event];
}

This guard ensures the window's keybinding processing runs only when the first responder is outside the WebView, avoiding double dispatch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- (void)sendEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown) {
// Process through keybinding system first
[self keyDown:event];
// Still pass to WKWebView for normal input handling
// (keybinding callbacks like Hide will take effect regardless)
}
[super sendEvent:event];
}
- (void)sendEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown) {
id responder = self.firstResponder;
BOOL responderInsideWebView = NO;
if ([responder isKindOfClass:[NSView class]] && self.webView != nil) {
responderInsideWebView = [((NSView *)responder) isDescendantOf:self.webView];
}
if (!responderInsideWebView) {
[self keyDown:event];
}
}
[super sendEvent:event];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.m` around lines 33 - 41, sendEvent:
is causing duplicate key handling by unconditionally invoking [self
keyDown:event] before [super sendEvent:event]; update the sendEvent:
implementations in WebviewWindow and WebviewPanel to only call [self
keyDown:event] when the window's firstResponder is not the WKWebView (or a
subview of it) — i.e., obtain the firstResponder from the window and skip the
manual keyDown dispatch if it isKindOfClass: WKWebView (or resides within the
WKWebView), otherwise call [self keyDown:event] and then [super sendEvent:event]
as before.

## [Unreleased]

### Added
- Added NSPanel support for macOS. Panels serve as auxillary windows and can appear over fullscreen apps without activating the application using `NSWindowStyleMaskNonactivatingPanel`. Configure via `Mac.WindowClass` and `Mac.PanelOptions` in window options. Added by [@Grantmartin2002](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix spelling typo in changelog entry.

Line 18: use “auxiliary” instead of “auxillary”.

✍️ Suggested edit
-- Added NSPanel support for macOS. Panels serve as auxillary windows and can appear over fullscreen apps without activating the application using `NSWindowStyleMaskNonactivatingPanel`. Configure via `Mac.WindowClass` and `Mac.PanelOptions` in window options. Added by [`@Grantmartin2002`](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
+- Added NSPanel support for macOS. Panels serve as auxiliary windows and can appear over fullscreen apps without activating the application using `NSWindowStyleMaskNonactivatingPanel`. Configure via `Mac.WindowClass` and `Mac.PanelOptions` in window options. Added by [`@Grantmartin2002`](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Added NSPanel support for macOS. Panels serve as auxillary windows and can appear over fullscreen apps without activating the application using `NSWindowStyleMaskNonactivatingPanel`. Configure via `Mac.WindowClass` and `Mac.PanelOptions` in window options. Added by [@Grantmartin2002](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
- Added NSPanel support for macOS. Panels serve as auxiliary windows and can appear over fullscreen apps without activating the application using `NSWindowStyleMaskNonactivatingPanel`. Configure via `Mac.WindowClass` and `Mac.PanelOptions` in window options. Added by [`@Grantmartin2002`](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
🧰 Tools
🪛 LanguageTool

[grammar] ~18-~18: Ensure spelling is correct
Context: ...anel support for macOS. Panels serve as auxillary windows and can appear over fullscreen ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/src/pages/changelog.mdx` at line 18, Fix the spelling typo in the
changelog entry: replace the misspelled word "auxillary" with "auxiliary" in the
sentence describing NSPanel support (the line mentioning "Panels serve as
auxillary windows..."); ensure the rest of the text (references to
`NSWindowStyleMaskNonactivatingPanel`, `Mac.WindowClass`, and
`Mac.PanelOptions`) remains unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
docs/src/content/docs/features/windows/options.mdx (1)

831-864: ⚠️ Potential issue | 🔴 Critical

Unescaped {} in prose will break MDX parsing (lines 831, 864).

In Astro Starlight .mdx files, a left curly brace { starts a JavaScript expression. Wrap these code references in backticks to render them literally.

Proposed fix
-**Note:** NSWindow fullscreen overlay requires application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }
+**Note:** NSWindow fullscreen overlay requires `application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }`

-**Note:** Unlike NSWindow, NSPanel fullscreen overlay does NOT require application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }
+**Note:** Unlike NSWindow, NSPanel fullscreen overlay does **not** require `application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/src/content/docs/features/windows/options.mdx` around lines 831 - 864,
The MDX contains unescaped curly-brace expressions that break parsing; wrap the
literal references containing braces in inline code ticks (e.g.,
`application.Options{ Mac: application.MacOptions{ ActivationPolicy:
application.ActivationPolicyAccessory } }`) and similarly backtick the examples
and type names that include braces or look like struct literals (references such
as `Mac: application.MacWindow{ ... }`,
`application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary`,
`application.MacWindowLevelFloating`, `MacWindowClass`, `NSWindow`, `NSPanel`,
`MacPanelOptions`, and the panel option names) so the `{}` are rendered
literally and MDX parsing is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@docs/src/content/docs/features/windows/options.mdx`:
- Around line 831-864: The MDX contains unescaped curly-brace expressions that
break parsing; wrap the literal references containing braces in inline code
ticks (e.g., `application.Options{ Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory } }`) and similarly
backtick the examples and type names that include braces or look like struct
literals (references such as `Mac: application.MacWindow{ ... }`,
`application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary`,
`application.MacWindowLevelFloating`, `MacWindowClass`, `NSWindow`, `NSPanel`,
`MacPanelOptions`, and the panel option names) so the `{}` are rendered
literally and MDX parsing is preserved.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc5af72 and 3884498.

📒 Files selected for processing (1)
  • docs/src/content/docs/features/windows/options.mdx

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.

1 participant