This commit is contained in:
Grant Martin 2026-03-06 21:53:16 +00:00 committed by GitHub
commit 688e05fcf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 270 additions and 144 deletions

View file

@ -828,8 +828,9 @@ Controls how the window behaves across macOS Spaces and fullscreen. These are bi
- `MacWindowCollectionBehaviorFullScreenAllowsTiling` - Allows side-by-side tiling (macOS 10.11+)
- `MacWindowCollectionBehaviorFullScreenDisallowsTiling` - Prevents tiling (macOS 10.11+)
**Example - Spotlight-like window:**
**Note:** MacWindowClassWindow fullscreen overlay requires `application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }`
**Example - Spotlight-like window:**
```go
// Window that appears on all Spaces AND can overlay fullscreen apps
Mac: application.MacWindow{
@ -848,6 +849,32 @@ Mac: application.MacWindow{
},
```
**WindowClass** (`MacWindowClass`)
- `MacWindowClassWindow` - Standard window (default)
- `MacWindowClassPanel` - Auxiliary window that can float above other windows and receive input without activating the application
**PanelPreferences** (`MacPanelPreferences`)
Preferences for MacWindowClassPanel windows (only applies when `WindowClass` is `MacWindowClassPanel`):
- `FloatingPanel` - Panel floats above other windows
- `BecomesKeyOnlyIfNeeded` - Panel becomes key only when needed
- `NonactivatingPanel` - Panel receives input without activating the application
- `UtilityWindow` - Panel uses utility window style
**Note:** Unlike MacWindowClassWindow, MacWindowClassPanel fullscreen overlay does NOT require `application.Options{ Mac: application.MacOptions{ ActivationPolicy: application.ActivationPolicyAccessory } }`
**Example - Spotlight-like panel:**
```go
Mac: application.MacWindow{
WindowClass: application.MacWindowClassPanel,
PanelPreferences: application.MacPanelPreferences{
NonactivatingPanel: true,
},
CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces |
application.MacWindowCollectionBehaviorFullScreenAuxiliary,
},
```
### Windows Options
```go

View file

@ -21,114 +21,134 @@ struct WebviewPreferences {
bool *AllowsBackForwardNavigationGestures;
};
struct PanelPreferences {
bool FloatingPanel;
bool BecomesKeyOnlyIfNeeded;
bool NonactivatingPanel;
bool UtilityWindow;
};
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) {
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
if (frameless) {
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
}
WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
// Note: collectionBehavior is set later via windowSetCollectionBehavior()
// to allow user configuration of Space and fullscreen behavior
// Create delegate
WebviewWindowDelegate* delegate = [[WebviewWindowDelegate alloc] init];
[delegate autorelease];
// Set delegate
[window setDelegate:delegate];
delegate.windowId = id;
// Add NSView to window
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, width-1, height-1)];
[view autorelease];
[view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
if( frameless ) {
[view setWantsLayer:YES];
view.layer.cornerRadius = 8.0;
}
[window setContentView:view];
// Embed wkwebview in window
// Shared helper to configure webview for a window or panel
static WKWebView* configureWebviewForWindow(NSWindow* window, NSView* view, WebviewWindowDelegate* delegate,
int width, int height, bool fraudulentWebsiteWarningEnabled,
bool enableDragAndDrop, struct WebviewPreferences webviewPreferences) {
NSRect frame = NSMakeRect(0, 0, width, height);
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
[config autorelease];
// Set preferences
if (preferences.TabFocusesLinks != NULL) {
config.preferences.tabFocusesLinks = *preferences.TabFocusesLinks;
if (webviewPreferences.TabFocusesLinks != NULL) {
config.preferences.tabFocusesLinks = *webviewPreferences.TabFocusesLinks;
}
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110300
if (@available(macOS 11.3, *)) {
if (preferences.TextInteractionEnabled != NULL) {
config.preferences.textInteractionEnabled = *preferences.TextInteractionEnabled;
if (webviewPreferences.TextInteractionEnabled != NULL) {
config.preferences.textInteractionEnabled = *webviewPreferences.TextInteractionEnabled;
}
}
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120300
if (@available(macOS 12.3, *)) {
if (preferences.FullscreenEnabled != NULL) {
config.preferences.elementFullscreenEnabled = *preferences.FullscreenEnabled;
}
}
#endif
config.suppressesIncrementalRendering = true;
config.applicationNameForUserAgent = @"wails.io";
[config setURLSchemeHandler:delegate forURLScheme:@"wails"];
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101500
if (@available(macOS 10.15, *)) {
config.preferences.fraudulentWebsiteWarningEnabled = fraudulentWebsiteWarningEnabled;
if (webviewPreferences.FullscreenEnabled != NULL) {
config.preferences.elementFullscreenEnabled = *webviewPreferences.FullscreenEnabled;
}
}
#endif
// Setup user content controller
WKUserContentController* userContentController = [WKUserContentController new];
[userContentController autorelease];
config.suppressesIncrementalRendering = true;
config.applicationNameForUserAgent = @"wails.io";
[config setURLSchemeHandler:delegate forURLScheme:@"wails"];
[userContentController addScriptMessageHandler:delegate name:@"external"];
config.userContentController = userContentController;
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101500
if (@available(macOS 10.15, *)) {
config.preferences.fraudulentWebsiteWarningEnabled = fraudulentWebsiteWarningEnabled;
}
#endif
WKUserContentController* userContentController = [WKUserContentController new];
[userContentController autorelease];
[userContentController addScriptMessageHandler:delegate name:@"external"];
config.userContentController = userContentController;
WKWebView* webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
[webView autorelease];
// Set allowsBackForwardNavigationGestures if specified
if (preferences.AllowsBackForwardNavigationGestures != NULL) {
webView.allowsBackForwardNavigationGestures = *preferences.AllowsBackForwardNavigationGestures;
}
[view addSubview:webView];
// support webview events
[webView setNavigationDelegate:delegate];
[webView setUIDelegate:delegate];
// Ensure webview resizes with the window
[webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
if( enableDragAndDrop ) {
WebviewDrag* dragView = [[WebviewDrag alloc] initWithFrame:NSMakeRect(0, 0, width-1, height-1)];
[dragView autorelease];
[view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[view addSubview:dragView];
dragView.windowId = id;
if (webviewPreferences.AllowsBackForwardNavigationGestures != NULL) {
webView.allowsBackForwardNavigationGestures = *webviewPreferences.AllowsBackForwardNavigationGestures;
}
window.webView = webView;
return window;
[view addSubview:webView];
[webView setNavigationDelegate:delegate];
[webView setUIDelegate:delegate];
[webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
if (enableDragAndDrop) {
WebviewDrag* dragView = [[WebviewDrag alloc] initWithFrame:NSMakeRect(0, 0, width-1, height-1)];
[dragView autorelease];
[view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[view addSubview:dragView];
dragView.windowId = delegate.windowId;
}
return webView;
}
// Create a new Window
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop,
struct WebviewPreferences webviewPreferences, bool isPanel, struct PanelPreferences panelPreferences) {
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
if (frameless) {
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
}
NSWindow* window;
if (isPanel) {
if (panelPreferences.NonactivatingPanel) {
styleMask |= NSWindowStyleMaskNonactivatingPanel;
}
if (panelPreferences.UtilityWindow) {
styleMask |= NSWindowStyleMaskUtilityWindow;
}
WebviewPanel* panel = [[WebviewPanel alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
[panel setFloatingPanel:panelPreferences.FloatingPanel];
[panel setBecomesKeyOnlyIfNeeded:panelPreferences.BecomesKeyOnlyIfNeeded];
window = panel;
} else {
window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
}
WebviewWindowDelegate* delegate = [[WebviewWindowDelegate alloc] init];
[delegate autorelease];
[window setDelegate:delegate];
delegate.windowId = id;
NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, width-1, height-1)];
[view autorelease];
[view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
if (frameless) {
[view setWantsLayer:YES];
view.layer.cornerRadius = 8.0;
}
[window setContentView:view];
WKWebView* webView = configureWebviewForWindow(window, view, delegate, width, height,
fraudulentWebsiteWarningEnabled, enableDragAndDrop, webviewPreferences);
if (isPanel) {
((WebviewPanel*)window).webView = webView;
} else {
((WebviewWindow*)window).webView = webView;
}
return window;
}
void printWindowStyle(void *window) {
WebviewWindow* nsWindow = (WebviewWindow*)window;
@ -1276,6 +1296,17 @@ func (w *macosWebviewWindow) getWebviewPreferences() C.struct_WebviewPreferences
return result
}
func (w *macosWebviewWindow) getPanelPreferences() C.struct_PanelPreferences {
panelPrefs := w.parent.options.Mac.PanelPreferences
return C.struct_PanelPreferences{
FloatingPanel: C.bool(panelPrefs.FloatingPanel),
BecomesKeyOnlyIfNeeded: C.bool(panelPrefs.BecomesKeyOnlyIfNeeded),
NonactivatingPanel: C.bool(panelPrefs.NonactivatingPanel),
UtilityWindow: C.bool(panelPrefs.UtilityWindow),
}
}
func (w *macosWebviewWindow) run() {
for eventId := range w.parent.eventListeners {
w.on(eventId)
@ -1291,6 +1322,8 @@ func (w *macosWebviewWindow) run() {
C.bool(options.Frameless),
C.bool(options.EnableFileDrop),
w.getWebviewPreferences(),
C.bool(macOptions.WindowClass == MacWindowClassPanel),
w.getPanelPreferences(),
)
w.setTitle(options.Title)
w.setResizable(!options.DisableResize)
@ -1320,10 +1353,13 @@ func (w *macosWebviewWindow) run() {
case MacBackdropNormal:
}
if macOptions.WindowLevel == "" {
macOptions.WindowLevel = MacWindowLevelNormal
// Only set window level if explicitly specified, or if not a floating panel
// (setFloatingPanel:YES already sets NSFloatingWindowLevel, so don't override it)
if macOptions.WindowLevel != "" {
w.setWindowLevel(macOptions.WindowLevel)
} else if !(macOptions.WindowClass == MacWindowClassPanel && macOptions.PanelPreferences.FloatingPanel) {
w.setWindowLevel(MacWindowLevelNormal)
}
w.setWindowLevel(macOptions.WindowLevel)
// Set collection behavior (defaults to FullScreenPrimary for backwards compatibility)
w.setCollectionBehavior(macOptions.CollectionBehavior)

View file

@ -18,6 +18,18 @@
@end
@interface WebviewPanel : NSPanel
- (BOOL) canBecomeKeyWindow;
- (BOOL) canBecomeMainWindow;
- (BOOL) acceptsFirstResponder;
- (BOOL) becomeFirstResponder;
- (BOOL) resignFirstResponder;
- (WebviewPanel*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation;
@property (assign) WKWebView* webView;
@end
@interface WebviewWindowDelegate : NSObject <NSWindowDelegate, WKScriptMessageHandler, WKNavigationDelegate, WKURLSchemeHandler, NSDraggingDestination, WKUIDelegate>
@property unsigned int windowId;

View file

@ -18,81 +18,22 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
LiquidGlassStyleDark = 2,
LiquidGlassStyleVibrant = 3
};
@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];
[self setAlphaValue:1.0];
[self setBackgroundColor:[NSColor clearColor]];
[self setOpaque:NO];
[self setMovableByWindowBackground:YES];
return self;
}
- (void)keyDown:(NSEvent *)event {
NSUInteger modifierFlags = event.modifierFlags;
// Create an array to hold the modifier strings
NSMutableArray *modifierStrings = [NSMutableArray array];
// Check for modifier flags and add corresponding strings to the array
if (modifierFlags & NSEventModifierFlagShift) {
[modifierStrings addObject:@"shift"];
}
if (modifierFlags & NSEventModifierFlagControl) {
[modifierStrings addObject:@"ctrl"];
}
if (modifierFlags & NSEventModifierFlagOption) {
[modifierStrings addObject:@"option"];
}
if (modifierFlags & NSEventModifierFlagCommand) {
[modifierStrings addObject:@"cmd"];
}
NSString *keyString = [self keyStringFromEvent:event];
if (keyString.length > 0) {
[modifierStrings addObject:keyString];
}
// Combine the modifier strings with the key character
NSString *keyEventString = [modifierStrings componentsJoinedByString:@"+"];
const char* utf8String = [keyEventString UTF8String];
WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)self.delegate;
processWindowKeyDownEvent(delegate.windowId, utf8String);
}
- (NSString *)keyStringFromEvent:(NSEvent *)event {
// Get the pressed key
// Check for special keys like escape and tab
// Shared key event handling functions
static NSString* keyStringFromKeyEvent(NSEvent *event) {
NSString *characters = [event characters];
if (characters.length == 0) {
return @"";
}
if ([characters isEqualToString:@"\r"]) {
return @"enter";
}
if ([characters isEqualToString:@"\b"]) {
return @"backspace";
}
if ([characters isEqualToString:@"\e"]) {
return @"escape";
}
// page down
if ([characters isEqualToString:@"\x0B"]) {
return @"page down";
}
// page up
if ([characters isEqualToString:@"\x0E"]) {
return @"page up";
}
// home
if ([characters isEqualToString:@"\x01"]) {
return @"home";
}
// end
if ([characters isEqualToString:@"\x04"]) {
return @"end";
}
// clear
if ([characters isEqualToString:@"\x0C"]) {
return @"clear";
}
if ([characters isEqualToString:@"\r"]) return @"enter";
if ([characters isEqualToString:@"\b"]) return @"backspace";
if ([characters isEqualToString:@"\e"]) return @"escape";
if ([characters isEqualToString:@"\x0B"]) return @"page down";
if ([characters isEqualToString:@"\x0E"]) return @"page up";
if ([characters isEqualToString:@"\x01"]) return @"home";
if ([characters isEqualToString:@"\x04"]) return @"end";
if ([characters isEqualToString:@"\x0C"]) return @"clear";
switch ([event keyCode]) {
// Function keys
case 122: return @"f1";
case 120: return @"f2";
case 99: return @"f3";
@ -113,7 +54,6 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
case 79: return @"f18";
case 80: return @"f19";
case 90: return @"f20";
// Letter keys
case 0: return @"a";
case 11: return @"b";
case 8: return @"c";
@ -140,7 +80,6 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
case 7: return @"x";
case 16: return @"y";
case 6: return @"z";
// Number keys
case 29: return @"0";
case 18: return @"1";
case 19: return @"2";
@ -151,7 +90,6 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
case 26: return @"7";
case 28: return @"8";
case 25: return @"9";
// Other special keys
case 51: return @"delete";
case 117: return @"forward delete";
case 123: return @"left";
@ -161,7 +99,6 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
case 48: return @"tab";
case 53: return @"escape";
case 49: return @"space";
// Punctuation and other keys (for a standard US layout)
case 33: return @"[";
case 30: return @"]";
case 43: return @",";
@ -176,6 +113,36 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
default: return @"";
}
}
static void dispatchKeyDownEvent(NSEvent *event, unsigned int windowId) {
NSUInteger modifierFlags = event.modifierFlags;
NSMutableArray *modifierStrings = [NSMutableArray array];
if (modifierFlags & NSEventModifierFlagShift) [modifierStrings addObject:@"shift"];
if (modifierFlags & NSEventModifierFlagControl) [modifierStrings addObject:@"ctrl"];
if (modifierFlags & NSEventModifierFlagOption) [modifierStrings addObject:@"option"];
if (modifierFlags & NSEventModifierFlagCommand) [modifierStrings addObject:@"cmd"];
NSString *keyString = keyStringFromKeyEvent(event);
if (keyString.length > 0) {
[modifierStrings addObject:keyString];
}
NSString *keyEventString = [modifierStrings componentsJoinedByString:@"+"];
processWindowKeyDownEvent(windowId, [keyEventString UTF8String]);
}
@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];
[self setAlphaValue:1.0];
[self setBackgroundColor:[NSColor clearColor]];
[self setOpaque:NO];
[self setMovableByWindowBackground:YES];
return self;
}
- (void)keyDown:(NSEvent *)event {
WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)self.delegate;
dispatchKeyDownEvent(event, delegate.windowId);
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
@ -243,6 +210,59 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
}
}
@end
@implementation WebviewPanel
- (WebviewPanel*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation;
{
self = [super initWithContentRect:contentRect styleMask:windowStyle backing:bufferingType defer:deferCreation];
[self setAlphaValue:1.0];
[self setBackgroundColor:[NSColor clearColor]];
[self setOpaque:NO];
[self setMovableByWindowBackground:YES];
return self;
}
// Override sendEvent to intercept key events BEFORE WKWebView consumes them
- (void)sendEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown) {
[self keyDown:event];
}
[super sendEvent:event];
}
- (void)keyDown:(NSEvent *)event {
WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)self.delegate;
dispatchKeyDownEvent(event, delegate.windowId);
}
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL) canBecomeMainWindow {
return NO; // Panels typically don't become main window
}
- (BOOL) acceptsFirstResponder {
return YES;
}
- (BOOL) becomeFirstResponder {
return YES;
}
- (BOOL) resignFirstResponder {
return YES;
}
- (void) setDelegate:(id<NSWindowDelegate>) delegate {
[delegate retain];
[super setDelegate: delegate];
if ([delegate isKindOfClass:[WebviewWindowDelegate class]]) {
[self registerForDraggedTypes:@[NSFilenamesPboardType]];
}
}
- (void) dealloc {
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"external"];
if (self.delegate) {
[self.delegate release];
}
[super dealloc];
}
@end
@implementation WebviewWindowDelegate
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
NSPasteboard *pasteboard = [sender draggingPasteboard];

View file

@ -485,6 +485,34 @@ type MacWindow struct {
// LiquidGlass contains configuration for the Liquid Glass effect
LiquidGlass MacLiquidGlass
// WindowClass is the window class for the window
WindowClass MacWindowClass
// PanelPreferences contains configuration for panel windows
PanelPreferences MacPanelPreferences
}
// MacWindowClass is the window class for macOS
type MacWindowClass int
const (
// MacWindowClassWindow - The default value. A window that an app displays on the screen.
MacWindowClassWindow MacWindowClass = iota
// MacWindowClassPanel - A special kind of window that typically performs a function that is auxiliary to the main window
MacWindowClassPanel
)
// MacPanelPreferences contains options for MacWindowClassPanel windows
type MacPanelPreferences struct {
// FloatingPanel will make the panel float above other windows
FloatingPanel bool
// BecomesKeyOnlyIfNeeded will make the panel become key only when needed
BecomesKeyOnlyIfNeeded bool
// NonactivatingPanel will apply the NSWindowStyleMaskNonactivatingPanel style
NonactivatingPanel bool
// UtilityWindow will apply the NSWindowStyleMaskUtilityWindow style
UtilityWindow bool
}
type MacWindowLevel string

View file

@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added Panel 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.PanelPreferences` in window options. Added by [@Grantmartin2002](https://github.com/Grantmartin2002) in [PR](https://github.com/wailsapp/wails/pull/5024)
### Fixed
- Fixed locking issue on Windows when multiselect dialog returns an error. Fixed in [PR](https://github.com/wailsapp/wails/pull/4156) by @johannes-luebke