Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/src/content/docs/features/windows/basics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,18 @@ childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
- `MacWindowCollectionBehaviorCanJoinAllSpaces` - Visible on all Spaces
- `MacWindowCollectionBehaviorFullScreenAuxiliary` - Can overlay fullscreen apps

**Non-activating panel (`Mac.NonActivatingPanel`):** Non-activating `NSPanel`—clicks don't activate your app or make this window main; it can still be key for typing. Often used with `Frameless`, a floating `WindowLevel`, and Space `CollectionBehavior`.

```go
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacWindow{
NonActivatingPanel: true,
WindowLevel: application.MacWindowLevelFloating,
},
})
```

**Native fullscreen:**
macOS fullscreen creates a new Space (virtual desktop).
</TabItem>
Expand Down
9 changes: 9 additions & 0 deletions docs/src/content/docs/features/windows/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,15 @@ Mac: application.MacWindow{
},
```

**NonActivatingPanel** (`bool`) — Non-activating `NSPanel` (`NSWindowStyleMaskNonactivatingPanel`): does not activate the app or become main; can become key for input. Often with `Frameless` and other `MacWindow` fields.

```go
Mac: application.MacWindow{
NonActivatingPanel: true,
WindowLevel: application.MacWindowLevelFloating,
},
```

### Windows Options

```go
Expand Down
38 changes: 31 additions & 7 deletions v3/pkg/application/webview_window_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ struct WebviewPreferences {
extern void registerListener(unsigned int event);

// Create a new Window
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, struct WebviewPreferences preferences) {
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, bool nonActivatingPanel, struct WebviewPreferences preferences) {
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
if (frameless) {
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
}
if (nonActivatingPanel) {
// NSWindowStyleMaskNonactivatingPanel only takes effect because
// WebviewWindow inherits from NSPanel. Setting this on a plain NSWindow
// is silently ignored by AppKit.
styleMask |= NSWindowStyleMaskNonactivatingPanel;
}
WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
styleMask:styleMask
backing:NSBackingStoreBuffered
Expand Down Expand Up @@ -698,7 +704,11 @@ void windowSetPositionOnScreen(void* nsWindow, int x, int y, const char* screenI

// Destroy window
void windowDestroy(void* nsWindow) {
[(WebviewWindow*)nsWindow close];
WebviewWindow* w = (WebviewWindow*)nsWindow;
[w close];
// releasedWhenClosed is NO (see WebviewWindow init), so -close does not
// release the object. Balance the -alloc from windowNew.
[w release];
}

// Remove drop shadow from window
Expand All @@ -715,7 +725,9 @@ void windowSetDisableEscapeExitsFullscreen(void* nsWindow, bool disable) {

// windowClose closes the current window
static void windowClose(void *window) {
[(WebviewWindow*)window close];
WebviewWindow* w = (WebviewWindow*)window;
[w close];
[w release];
}

// windowZoom
Expand Down Expand Up @@ -906,8 +918,12 @@ void windowSetEnabled(void *window, bool enabled) {

void windowFocus(void *window) {
WebviewWindow* nsWindow = (WebviewWindow*)window;
// If the current application is not active, activate it
if (![[NSApplication sharedApplication] isActive]) {
// Skip the app activation step for non-activating panels - that's the entire
// point of NSWindowStyleMaskNonactivatingPanel. The panel itself can still
// become key (so text fields work) without stealing focus from whichever app
// the user was using.
BOOL isNonActivatingPanel = ([nsWindow styleMask] & NSWindowStyleMaskNonactivatingPanel) != 0;
if (!isNonActivatingPanel && ![[NSApplication sharedApplication] isActive]) {
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
}
[nsWindow makeKeyAndOrderFront:nil];
Expand Down Expand Up @@ -1096,7 +1112,11 @@ func (w *macosWebviewWindow) close() {
globalApplication.debug("Window close() called - setting unconditionallyClose flag", "windowId", w.parent.id, "title", w.parent.options.Title)
// Set the unconditionallyClose flag to allow the window to close
atomic.StoreUint32(&w.parent.unconditionallyClose, 1)
C.windowClose(w.nsWindow)
if w.nsWindow != nil {
C.windowClose(w.nsWindow)
// windowClose releases the ObjC object; clear the pointer so we never pass a dangling reference to C.
w.nsWindow = nil
}
globalApplication.debug("Window close() completed", "windowId", w.parent.id, "title", w.parent.options.Title)
// TODO: Check if we need to unregister the window here or not
}
Expand Down Expand Up @@ -1356,6 +1376,7 @@ func (w *macosWebviewWindow) run() {
C.bool(macOptions.EnableFraudulentWebsiteWarnings),
C.bool(options.Frameless),
C.bool(options.EnableFileDrop),
C.bool(macOptions.NonActivatingPanel),
w.getWebviewPreferences(),
)
if macOptions.DisableEscapeExitsFullscreen {
Expand Down Expand Up @@ -1610,7 +1631,10 @@ func (w *macosWebviewWindow) destroy() {
w.parent.markAsDestroyed()
// Clear caches for this window
clearWindowDragCache(w.parent.id)
C.windowDestroy(w.nsWindow)
if w.nsWindow != nil {
C.windowDestroy(w.nsWindow)
w.nsWindow = nil
}
}

func (w *macosWebviewWindow) setHTML(html string) {
Expand Down
8 changes: 7 additions & 1 deletion v3/pkg/application/webview_window_darwin.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>

@interface WebviewWindow : NSWindow
// WebviewWindow inherits from NSPanel (not NSWindow) so windows opted in to
// NSWindowStyleMaskNonactivatingPanel actually honor that style. NSPanel's
// default hidesOnDeactivate=YES is cleared in webview_window_darwin.m so
// windows stay visible when another app is active. releasedWhenClosed is NO
// on purpose: the Go bridge owns the object and windowClose/windowDestroy
// must -release it (see webview_window_darwin.go).
@interface WebviewWindow : NSPanel
- (BOOL) canBecomeKeyWindow;
- (BOOL) canBecomeMainWindow;
- (BOOL) acceptsFirstResponder;
Expand Down
14 changes: 14 additions & 0 deletions v3/pkg/application/webview_window_darwin.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ @implementation WebviewWindow
- (WebviewWindow*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation;
{
self = [super initWithContentRect:contentRect styleMask:windowStyle backing:bufferingType defer:deferCreation];
// Use releasedWhenClosed=NO so closing never implicitly -releases the object;
// Go keeps an unsafe.Pointer to the NSWindow until native teardown runs
// windowClose/windowDestroy, which must -release to balance windowNew's -alloc.
[self setReleasedWhenClosed:NO];
// NSPanel defaults hidesOnDeactivate=YES so panels hide when the app resigns active;
// normalize to NSWindow-like NO so ordinary windows stay visible on app switch.
[self setHidesOnDeactivate:NO];
[self setAlphaValue:1.0];
[self setBackgroundColor:[NSColor clearColor]];
[self setOpaque:NO];
Expand Down Expand Up @@ -177,9 +184,16 @@ - (NSString *)keyStringFromEvent:(NSEvent *)event {
}
}
- (BOOL)canBecomeKeyWindow {
// Even non-activating panels need to become key so text inputs work.
return YES;
}
- (BOOL) canBecomeMainWindow {
// Non-activating panels (NSWindowStyleMaskNonactivatingPanel) must never
// become the application's main window — that's the canonical accessory
// panel pattern. All other windows behave as before.
if (([self styleMask] & NSWindowStyleMaskNonactivatingPanel) != 0) {
return NO;
}
return YES;
}
- (BOOL) acceptsFirstResponder {
Expand Down
16 changes: 16 additions & 0 deletions v3/pkg/application/webview_window_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,22 @@ type MacWindow struct {
// web content (e.g. modals with Esc-to-close behaviour) to handle Esc directly.
// Default false preserves standard macOS behaviour where Esc exits fullscreen.
DisableEscapeExitsFullscreen bool

// NonActivatingPanel makes the window an NSPanel with the
// NSWindowStyleMaskNonactivatingPanel style: interacting with it does not
// activate the owning app or steal focus from the active app. The window
// can still become key (so text input works) but never main.
//
// Typical use: floating tool palettes, Spotlight-style overlays, menu-bar
// detail panels — usually combined with other window-scoped MacWindow
// fields (WindowLevel, CollectionBehavior, Backdrop) plus Frameless on
// WebviewWindowOptions.
//
// To also hide the dock icon, set ActivationPolicyAccessory at the
// *application* level via Options.Mac.ActivationPolicy. Options.Mac is
// MacOptions and is distinct from WebviewWindowOptions.Mac (this struct,
// MacWindow) — same field name, different scopes.
NonActivatingPanel bool
}

type MacWindowLevel string
Expand Down
40 changes: 40 additions & 0 deletions v3/test/manual/macos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# macOS Manual Tests

Manual test programs for macOS-specific window behavior. These tests can't be
automated in `go test` because they rely on AppKit-level activation state and
visual cues that only a human can verify.

## Running

```bash
cd v3/test/manual/macos/<test-name>
go run .
```

## Tests

### non-activating-panel

Verifies `MacWindow.NonActivatingPanel` and the underlying `NSPanel` migration.

Opens two windows side by side: one with `NonActivatingPanel: true` (the
"Panel"), and one without (the "Normal" window, used as a control).

| # | Action | Expected behavior |
|---|--------|-------------------|
| 1 | Bring another app to the foreground (e.g. Finder) | Both windows STAY VISIBLE. Regression check for `hidesOnDeactivate` — if the panel vanishes on app switch, the `NSPanel` default leaked through. |
| 2 | Click the input field in the **Panel** window | Cursor blinks in the input; the *other* app's menu bar stays in place. The Wails app does not become active in the dock. |
| 3 | Click the input field in the **Normal** window | Cursor blinks in the input; Wails *does* activate (menu bar switches, dock icon highlights). This is the control case. |
| 4 | Open the Window menu in Wails' menu bar (after clicking the Normal window) | Only the Normal window appears in the list. The Panel never becomes the app's main window. |
| 5 | Type into the Panel's input | Characters appear (proves the panel becomes key even though it doesn't activate the app). |
| 6 | Close the Panel via Cmd+W, then quit and re-run | No crash. Regression check for `releasedWhenClosed` — `NSPanel` defaults that to `YES` and the Go side keeps a raw pointer past `[close]`. |

### Notes

- Step 2 is the headline behavior. If it fails, the `windowFocus` /
`activateIgnoringOtherApps:` guard or the `NSWindowStyleMaskNonactivatingPanel`
bit isn't being honored.
- Step 1 is the regression most likely to be re-introduced — easy to forget
that `NSPanel` differs from `NSWindow` on `hidesOnDeactivate`.
- Step 4 verifies the conditional `canBecomeMainWindow` override in
`webview_window_darwin.m`.
84 changes: 84 additions & 0 deletions v3/test/manual/macos/non-activating-panel/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Manual test for MacWindow.NonActivatingPanel.
//
// Opens two windows side by side:
// - "Panel" - NonActivatingPanel: true (the feature under test)
// - "Normal" - default WebviewWindow (the control)
//
// See ../README.md for the verification checklist.
//
//go:build darwin

package main

import (
"log"

"github.com/wailsapp/wails/v3/pkg/application"
)

const panelHTML = `<!DOCTYPE html>
<html><head><style>
body { margin: 0; padding: 20px; font: 14px -apple-system;
color: #fff; background: rgba(30,30,30,0.85);
-webkit-app-region: drag; height: 100vh; box-sizing: border-box; }
h1 { margin: 0 0 6px; font-size: 15px; }
p { margin: 0 0 12px; opacity: 0.7; font-size: 12px; }
input { -webkit-app-region: no-drag; width: 100%; padding: 8px;
background: rgba(255,255,255,0.1); color: #fff;
border: 1px solid rgba(255,255,255,0.25); border-radius: 4px; font: 13px monospace; }
</style></head><body>
<h1>Non-Activating Panel</h1>
<p>Click the input. Wails should NOT take focus from your other app.</p>
<input placeholder="Type to verify text input still works" autofocus />
</body></html>`

const normalHTML = `<!DOCTYPE html>
<html><head><style>
body { margin: 0; padding: 20px; font: 14px -apple-system;
color: #222; background: #f5f5f5; height: 100vh; box-sizing: border-box; }
h1 { margin: 0 0 6px; font-size: 15px; }
p { margin: 0 0 12px; color: #555; font-size: 12px; }
input { width: 100%; padding: 8px; border: 1px solid #ccc;
border-radius: 4px; font: 13px monospace; }
</style></head><body>
<h1>Normal Window (control)</h1>
<p>Click the input. Wails SHOULD activate (compare with the panel).</p>
<input placeholder="Type to verify text input works" />
</body></html>`

func main() {
app := application.New(application.Options{
Name: "NonActivatingPanel manual test",
})

app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Panel",
Width: 360,
Height: 180,
X: 400,
Y: 300,
Frameless: true,
HTML: panelHTML,
Mac: application.MacWindow{
NonActivatingPanel: true,
WindowLevel: application.MacWindowLevelFloating,
Backdrop: application.MacBackdropTranslucent,
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary,
},
})

app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Normal",
Width: 360,
Height: 180,
X: 790,
Y: 300,
HTML: normalHTML,
})

log.Println("Two windows opened. See README.md for the verification checklist.")
if err := app.Run(); err != nil {
log.Fatal(err)
}
}