diff --git a/v3/Taskfile.yaml b/v3/Taskfile.yaml index 6f9899091..04afd60f4 100644 --- a/v3/Taskfile.yaml +++ b/v3/Taskfile.yaml @@ -184,6 +184,7 @@ tasks: html-dnd-api ignore-mouse keybindings + liquid-glass menu notifications panic-handling @@ -308,6 +309,7 @@ tasks: html-dnd-api ignore-mouse keybindings + liquid-glass menu notifications panic-handling diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 8e4648038..57112da92 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add native Liquid Glass effect support for macOS with NSGlassEffectView (macOS 15.0+) and NSVisualEffectView fallback, including comprehensive material customization options by @leaanthony in [#4534](https://github.com/wailsapp/wails/pull/4534) ## Changed diff --git a/v3/examples/liquid-glass/.gitignore b/v3/examples/liquid-glass/.gitignore new file mode 100644 index 000000000..45d2406b0 --- /dev/null +++ b/v3/examples/liquid-glass/.gitignore @@ -0,0 +1 @@ +liquid-glass-demo diff --git a/v3/examples/liquid-glass/README.md b/v3/examples/liquid-glass/README.md new file mode 100644 index 000000000..33875af54 --- /dev/null +++ b/v3/examples/liquid-glass/README.md @@ -0,0 +1,70 @@ +# Liquid Glass Demo for Wails v3 + +This demo showcases the native Liquid Glass effect available in macOS 15.0+ with fallback to NSVisualEffectView for older systems. + +## Features Demonstrated + +### Window Styles + +1. **Light Glass** - Clean, light appearance with no tint +2. **Dark Glass** - Dark themed glass effect +3. **Vibrant Glass** - Enhanced vibrant effect for maximum transparency +4. **Tinted Glass** - Blue tinted glass with custom RGBA color +5. **Sheet Material** - Using specific NSVisualEffectMaterialSheet +6. **HUD Window** - Ultra-light HUD window material +7. **Content Background** - Content background material with warm tint + +### Customization Options + +- **Style**: `LiquidGlassStyleAutomatic`, `LiquidGlassStyleLight`, `LiquidGlassStyleDark`, `LiquidGlassStyleVibrant` +- **Material**: Direct NSVisualEffectMaterial selection (when NSGlassEffectView is not available) + - `NSVisualEffectMaterialAppearanceBased` + - `NSVisualEffectMaterialLight` + - `NSVisualEffectMaterialDark` + - `NSVisualEffectMaterialSheet` + - `NSVisualEffectMaterialHUDWindow` + - `NSVisualEffectMaterialContentBackground` + - `NSVisualEffectMaterialUnderWindowBackground` + - `NSVisualEffectMaterialUnderPageBackground` + - And more... +- **CornerRadius**: Rounded corners (0 for square corners) +- **TintColor**: Custom RGBA tint overlay +- **GroupID**: For grouping multiple glass windows (future feature) +- **GroupSpacing**: Spacing between grouped windows (future feature) + +### Running the Demo + +```bash +go build -o liquid-glass-demo . +./liquid-glass-demo +``` + +### Requirements + +- macOS 10.14+ (best experience on macOS 26.0+ with native NSGlassEffectView) +- Wails v3 + +### Implementation Details + +The implementation uses: +- Native `NSGlassEffectView` on macOS 26.0+ for authentic glass effect +- Falls back to `NSVisualEffectView` on older systems +- Runtime detection using `NSClassFromString` for compatibility +- Key-Value Coding (KVC) for dynamic property setting + +### Example Usage + +```go +window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, // Make window draggable + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleLight, + Material: application.NSVisualEffectMaterialHUDWindow, + CornerRadius: 20.0, + TintColor: &application.RGBA{Red: 0, Green: 100, Blue: 200, Alpha: 50}, + }, + }, +}) +``` \ No newline at end of file diff --git a/v3/examples/liquid-glass/index.html b/v3/examples/liquid-glass/index.html new file mode 100644 index 000000000..7f9ff4545 --- /dev/null +++ b/v3/examples/liquid-glass/index.html @@ -0,0 +1,50 @@ + + + + + + Wails Liquid Glass + + + +
+ +

LIQUID GLASS

+
+ + \ No newline at end of file diff --git a/v3/examples/liquid-glass/main.go b/v3/examples/liquid-glass/main.go new file mode 100644 index 000000000..9562c5930 --- /dev/null +++ b/v3/examples/liquid-glass/main.go @@ -0,0 +1,235 @@ +package main + +import ( + _ "embed" + "encoding/base64" + "log" + "runtime" + "strings" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed index.html +var indexHTML string + +//go:embed wails-logo.png +var wailsLogo []byte + +func main() { + app := application.New(application.Options{ + Name: "Wails Liquid Glass Demo", + Description: "Demonstrates the native Liquid Glass effect on macOS", + }) + + // Check if running on macOS + if runtime.GOOS != "darwin" { + // Show dialog for non-macOS platforms + application.InfoDialog(). + SetTitle("macOS Only Demo"). + SetMessage("The Liquid Glass effect is a macOS-specific feature that uses native NSGlassEffectView (macOS 15.0+) or NSVisualEffectView.\n\nThis demo is not available on " + runtime.GOOS + "."). + Show() + return + } + + // Convert logo to base64 data URI + logoDataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(wailsLogo) + + // Create different HTML for each window + lightHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + lightHTML = strings.Replace(lightHTML, "LIQUID GLASS", "Light Style", 1) + + darkHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + darkHTML = strings.Replace(darkHTML, "LIQUID GLASS", "Dark Style", 1) + + vibrantHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + vibrantHTML = strings.Replace(vibrantHTML, "LIQUID GLASS", "Vibrant Style", 1) + + tintedHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + tintedHTML = strings.Replace(tintedHTML, "LIQUID GLASS", "Blue Tint", 1) + + sheetHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + sheetHTML = strings.Replace(sheetHTML, "LIQUID GLASS", "Sheet Material", 1) + + hudHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + hudHTML = strings.Replace(hudHTML, "LIQUID GLASS", "HUD Window", 1) + + contentHTML := strings.Replace(indexHTML, "wails-logo.png", logoDataURI, 1) + contentHTML = strings.Replace(contentHTML, "LIQUID GLASS", "Content Background", 1) + + // Window 1: Light style with no tint + window1 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Light Glass", + Width: 350, + Height: 280, + X: 100, + Y: 100, + Frameless: true, + EnableDragAndDrop: false, + HTML: lightHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleLight, + Material: application.NSVisualEffectMaterialAuto, + CornerRadius: 20.0, + TintColor: nil, + }, + }, + }) + + // Window 2: Dark style + window2 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Dark Glass", + Width: 350, + Height: 280, + X: 500, + Y: 100, + Frameless: true, + EnableDragAndDrop: false, + HTML: darkHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleDark, + Material: application.NSVisualEffectMaterialAuto, + CornerRadius: 20.0, + TintColor: nil, + }, + }, + }) + + // Window 3: Vibrant style + window3 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Vibrant Glass", + Width: 350, + Height: 280, + X: 900, + Y: 100, + Frameless: true, + EnableDragAndDrop: false, + HTML: vibrantHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleVibrant, + Material: application.NSVisualEffectMaterialAuto, + CornerRadius: 20.0, + TintColor: nil, + }, + }, + }) + + // Window 4: Blue tinted glass + window4 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Tinted Glass", + Width: 350, + Height: 280, + X: 300, + Y: 420, + Frameless: true, + EnableDragAndDrop: false, + HTML: tintedHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleLight, + Material: application.NSVisualEffectMaterialAuto, + CornerRadius: 25.0, // Different corner radius + TintColor: &application.RGBA{Red: 0, Green: 100, Blue: 200, Alpha: 50}, // Blue tint + }, + }, + }) + + // Window 5: Using specific NSVisualEffectMaterial + window5 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Sheet Material", + Width: 350, + Height: 280, + X: 700, + Y: 420, + Frameless: true, + EnableDragAndDrop: false, + HTML: sheetHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleAutomatic, // Automatic style + Material: application.NSVisualEffectMaterialSheet, // Specific material + CornerRadius: 15.0, // Different corner radius + TintColor: nil, + }, + }, + }) + + // Window 6: HUD Window Material (very light, translucent) + window6 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "HUD Window", + Width: 350, + Height: 280, + X: 100, + Y: 740, + Frameless: true, + EnableDragAndDrop: false, + HTML: hudHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleAutomatic, + Material: application.NSVisualEffectMaterialHUDWindow, // HUD Window material - very light + CornerRadius: 30.0, // Larger corner radius + TintColor: nil, + }, + }, + }) + + // Window 7: Content Background Material + window7 := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Content Background", + Width: 350, + Height: 280, + X: 500, + Y: 740, + Frameless: true, + EnableDragAndDrop: false, + HTML: contentHTML, + InitialPosition: application.WindowXY, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropLiquidGlass, + InvisibleTitleBarHeight: 500, + LiquidGlass: application.MacLiquidGlass{ + Style: application.LiquidGlassStyleAutomatic, + Material: application.NSVisualEffectMaterialContentBackground, // Content background + CornerRadius: 10.0, // Smaller corner radius + TintColor: &application.RGBA{Red: 0, Green: 200, Blue: 100, Alpha: 30}, // Warm tint + }, + }, + }) + + // Show all windows + window1.Show() + window2.Show() + window3.Show() + window4.Show() + window5.Show() + window6.Show() + window7.Show() + + // Run the application + err := app.Run() + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/liquid-glass/wails-logo.png b/v3/examples/liquid-glass/wails-logo.png new file mode 100644 index 000000000..e65c582ff Binary files /dev/null and b/v3/examples/liquid-glass/wails-logo.png differ diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 8d5a85a8e..dfab963cb 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -4,7 +4,7 @@ package application /* #cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c -#cgo LDFLAGS: -framework Cocoa -framework WebKit +#cgo LDFLAGS: -framework Cocoa -framework WebKit -framework QuartzCore #include "application_darwin.h" #include "webview_window_darwin.h" @@ -1247,6 +1247,8 @@ func (w *macosWebviewWindow) run() { case MacBackdropTranslucent: C.windowSetTranslucent(w.nsWindow) C.webviewSetTransparent(w.nsWindow) + case MacBackdropLiquidGlass: + w.applyLiquidGlass() case MacBackdropNormal: } @@ -1351,6 +1353,55 @@ func (w *macosWebviewWindow) setBackgroundColour(colour RGBA) { C.windowSetBackgroundColour(w.nsWindow, C.int(colour.Red), C.int(colour.Green), C.int(colour.Blue), C.int(colour.Alpha)) } +func (w *macosWebviewWindow) applyLiquidGlass() { + options := w.parent.options.Mac.LiquidGlass + + // Validate corner radius + if options.CornerRadius < 0 { + options.CornerRadius = 0 + } + + globalApplication.debug("Applying Liquid Glass effect", "window", w.parent.id) + + // Check if liquid glass is supported + if !C.isLiquidGlassSupported() { + // Fallback to translucent + C.windowSetTranslucent(w.nsWindow) + C.webviewSetTransparent(w.nsWindow) + globalApplication.debug("Liquid Glass not supported on this macOS version, falling back to translucent", "window", w.parent.id) + return + } + + // Prepare tint color values (already clamped by uint8 type) + var r, g, b, a C.int + if options.TintColor != nil { + r = C.int(options.TintColor.Red) + g = C.int(options.TintColor.Green) + b = C.int(options.TintColor.Blue) + a = C.int(options.TintColor.Alpha) + } + + // Prepare group ID + var groupIDCStr *C.char + if options.GroupID != "" { + groupIDCStr = C.CString(options.GroupID) + defer C.free(unsafe.Pointer(groupIDCStr)) + } + + // Apply liquid glass effect + C.windowSetLiquidGlass( + w.nsWindow, + C.int(options.Style), + C.int(options.Material), + C.double(options.CornerRadius), + r, g, b, a, + groupIDCStr, + C.double(options.GroupSpacing), + ) + + globalApplication.debug("Applied Liquid Glass effect", "window", w.parent.id, "style", options.Style) +} + func (w *macosWebviewWindow) relativePosition() (int, int) { var x, y C.int InvokeSync(func() { diff --git a/v3/pkg/application/webview_window_darwin.h b/v3/pkg/application/webview_window_darwin.h index f0f58d896..0743fd6d9 100644 --- a/v3/pkg/application/webview_window_darwin.h +++ b/v3/pkg/application/webview_window_darwin.h @@ -34,4 +34,12 @@ void windowSetScreen(void* window, void* screen, int yOffset); +// Liquid Glass support functions +bool isLiquidGlassSupported(); +void windowSetLiquidGlass(void* window, int style, int material, double cornerRadius, + int r, int g, int b, int a, + const char* groupID, double groupSpacing); +void windowRemoveVisualEffects(void* window); +void configureWebViewForLiquidGlass(void* window); + #endif /* WebviewWindowDelegate_h */ diff --git a/v3/pkg/application/webview_window_darwin.m b/v3/pkg/application/webview_window_darwin.m index 033792c97..199556bc6 100644 --- a/v3/pkg/application/webview_window_darwin.m +++ b/v3/pkg/application/webview_window_darwin.m @@ -1,6 +1,7 @@ //go:build darwin #import #import +#import #import "webview_window_darwin.h" #import "../events/events_darwin.h" extern void processMessage(unsigned int, const char*); @@ -9,6 +10,14 @@ extern void processDragItems(unsigned int windowId, char** arr, int length, int extern void processWindowKeyDownEvent(unsigned int, const char*); extern bool hasListeners(unsigned int); extern bool windowShouldUnconditionallyClose(unsigned int); + +// Define NSGlassEffectView style constants (these match the Go constants) +typedef NS_ENUM(NSInteger, NSGlassEffectViewStyle) { + NSGlassEffectViewStyleAutomatic = 0, + NSGlassEffectViewStyleLight = 1, + NSGlassEffectViewStyleDark = 2, + NSGlassEffectViewStyleVibrant = 3 +}; @implementation WebviewWindow - (WebviewWindow*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation; { @@ -321,14 +330,11 @@ extern bool windowShouldUnconditionallyClose(unsigned int); - (BOOL)windowShouldClose:(NSWindow *)sender { WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate]; - NSLog(@"[DEBUG] windowShouldClose called for window %d", delegate.windowId); // Check if this window should close unconditionally (called from Close() method) if (windowShouldUnconditionallyClose(delegate.windowId)) { - NSLog(@"[DEBUG] Window %d closing unconditionally (Close() method called)", delegate.windowId); return true; } // For user-initiated closes, emit WindowClosing event and let the application decide - NSLog(@"[DEBUG] Window %d close requested by user - emitting WindowClosing event", delegate.windowId); processWindowEvent(delegate.windowId, EventWindowShouldClose); return false; } @@ -544,13 +550,11 @@ extern bool windowShouldUnconditionallyClose(unsigned int); } } - (void)windowDidOrderOffScreen:(NSNotification *)notification { - NSLog(@"[DEBUG] Window %d ordered OFF screen (hidden)", self.windowId); if( hasListeners(EventWindowDidOrderOffScreen) ) { processWindowEvent(self.windowId, EventWindowDidOrderOffScreen); } } - (void)windowDidOrderOnScreen:(NSNotification *)notification { - NSLog(@"[DEBUG] Window %d ordered ON screen (shown)", self.windowId); if( hasListeners(EventWindowDidOrderOnScreen) ) { processWindowEvent(self.windowId, EventWindowDidOrderOnScreen); } @@ -626,7 +630,6 @@ extern bool windowShouldUnconditionallyClose(unsigned int); } } - (void)windowWillClose:(NSNotification *)notification { - NSLog(@"[DEBUG] Window %d WILL close (window is actually closing)", self.windowId); if( hasListeners(EventWindowWillClose) ) { processWindowEvent(self.windowId, EventWindowWillClose); } @@ -672,13 +675,11 @@ extern bool windowShouldUnconditionallyClose(unsigned int); } } - (void)windowWillOrderOffScreen:(NSNotification *)notification { - NSLog(@"[DEBUG] Window %d WILL order off screen (about to hide)", self.windowId); if( hasListeners(EventWindowWillOrderOffScreen) ) { processWindowEvent(self.windowId, EventWindowWillOrderOffScreen); } } - (void)windowWillOrderOnScreen:(NSNotification *)notification { - NSLog(@"[DEBUG] Window %d WILL order on screen (about to show)", self.windowId); if( hasListeners(EventWindowWillOrderOnScreen) ) { processWindowEvent(self.windowId, EventWindowWillOrderOnScreen); } @@ -815,3 +816,234 @@ void windowSetScreen(void* window, void* screen, int yOffset) { // Set the frame which moves the window to the new screen [nsWindow setFrame:frame display:YES]; } + +// Check if Liquid Glass is supported on this system +bool isLiquidGlassSupported() { + // Check for macOS 26.0+ and NSGlassEffectView availability + if (@available(macOS 26.0, *)) { + return NSClassFromString(@"NSGlassEffectView") != nil; + } + return false; +} + +// Remove any existing visual effects from the window +void windowRemoveVisualEffects(void* nsWindow) { + WebviewWindow* window = (WebviewWindow*)nsWindow; + NSView* contentView = [window contentView]; + + // Get NSGlassEffectView class if available (avoid hard reference) + Class glassEffectViewClass = nil; + if (@available(macOS 26.0, *)) { + glassEffectViewClass = NSClassFromString(@"NSGlassEffectView"); + } + + // Remove all NSVisualEffectView and NSGlassEffectView subviews + NSArray* subviews = [contentView subviews]; + for (NSView* subview in subviews) { + if ([subview isKindOfClass:[NSVisualEffectView class]] || + (glassEffectViewClass && [subview isKindOfClass:glassEffectViewClass])) { + [subview removeFromSuperview]; + } + } +} + +// Configure WebView for liquid glass effect +void configureWebViewForLiquidGlass(void* nsWindow) { + WebviewWindow* window = (WebviewWindow*)nsWindow; + WKWebView* webView = window.webView; + + // Make WebView background transparent + [webView setValue:@NO forKey:@"drawsBackground"]; + if (@available(macOS 10.12, *)) { + [webView setValue:[NSColor clearColor] forKey:@"backgroundColor"]; + } + + // Ensure WebView is above glass layer + if (webView.layer) { + webView.layer.zPosition = 1.0; + webView.layer.shouldRasterize = YES; + webView.layer.rasterizationScale = [[NSScreen mainScreen] backingScaleFactor]; + } +} + +// Apply Liquid Glass effect to window +void windowSetLiquidGlass(void* nsWindow, int style, int material, double cornerRadius, + int r, int g, int b, int a, + const char* groupID, double groupSpacing) { + WebviewWindow* window = (WebviewWindow*)nsWindow; + + // Ensure we're on the main thread for UI operations + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + windowSetLiquidGlass(nsWindow, style, material, cornerRadius, r, g, b, a, groupID, groupSpacing); + }); + return; + } + + // Remove any existing visual effects + windowRemoveVisualEffects(nsWindow); + + // Try to use NSGlassEffectView if available + NSView* glassView = nil; + + if (@available(macOS 26.0, *)) { + Class NSGlassEffectViewClass = NSClassFromString(@"NSGlassEffectView"); + if (NSGlassEffectViewClass) { + // Create NSGlassEffectView (autoreleased) + glassView = [[[NSGlassEffectViewClass alloc] init] autorelease]; + + // Set corner radius if the property exists + if (cornerRadius > 0 && [glassView respondsToSelector:@selector(setCornerRadius:)]) { + [glassView setValue:@(cornerRadius) forKey:@"cornerRadius"]; + } + + // Set tint color if the property exists and color is specified + if (a > 0 && [glassView respondsToSelector:@selector(setTintColor:)]) { + NSColor* tintColor = [NSColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a/255.0]; + // Use performSelector to safely set tintColor if the setter exists + [glassView performSelector:@selector(setTintColor:) withObject:tintColor]; + } + + // Set style if the property exists + if ([glassView respondsToSelector:@selector(setStyle:)]) { + // For vibrant style, try to use Light style for a lighter effect + int lightStyle = (style == NSGlassEffectViewStyleVibrant) ? NSGlassEffectViewStyleLight : style; + [glassView setValue:@(lightStyle) forKey:@"style"]; + } + + // Set group identifier if the property exists and groupID is specified + if (groupID && strlen(groupID) > 0) { + if ([glassView respondsToSelector:@selector(setGroupIdentifier:)]) { + NSString* groupIDString = [NSString stringWithUTF8String:groupID]; + [glassView performSelector:@selector(setGroupIdentifier:) withObject:groupIDString]; + } else if ([glassView respondsToSelector:@selector(setGroupName:)]) { + NSString* groupIDString = [NSString stringWithUTF8String:groupID]; + [glassView performSelector:@selector(setGroupName:) withObject:groupIDString]; + } + } + + // Set group spacing if the property exists and spacing is specified + if (groupSpacing > 0 && [glassView respondsToSelector:@selector(setGroupSpacing:)]) { + [glassView setValue:@(groupSpacing) forKey:@"groupSpacing"]; + } + } + } + + // Fallback to NSVisualEffectView if NSGlassEffectView is not available + if (!glassView) { + NSVisualEffectView* effectView = [[[NSVisualEffectView alloc] init] autorelease]; + glassView = effectView; // Use effectView as glassView for the rest of the function + + // If a custom material is specified, use it directly + if (material >= 0) { + [effectView setMaterial:(NSVisualEffectMaterial)material]; + [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + } else { + // Configure the visual effect based on style + switch(style) { + case 1: // Light + if (@available(macOS 15.0, *)) { + [effectView setMaterial:NSVisualEffectMaterialUnderPageBackground]; + } else if (@available(macOS 10.14, *)) { + [effectView setMaterial:NSVisualEffectMaterialHUDWindow]; + } else { + [effectView setMaterial:NSVisualEffectMaterialLight]; + } + [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + break; + case 2: // Dark + if (@available(macOS 15.0, *)) { + [effectView setMaterial:NSVisualEffectMaterialHeaderView]; + } else if (@available(macOS 10.14, *)) { + [effectView setMaterial:NSVisualEffectMaterialFullScreenUI]; + } else { + [effectView setMaterial:NSVisualEffectMaterialDark]; + } + [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + break; + case 3: // Vibrant + if (@available(macOS 11.0, *)) { + // Use the lightest material available - similar to dock + [effectView setMaterial:NSVisualEffectMaterialHUDWindow]; + } else if (@available(macOS 10.14, *)) { + [effectView setMaterial:NSVisualEffectMaterialSheet]; + } else { + [effectView setMaterial:NSVisualEffectMaterialLight]; + } + // Use behind window for true transparency + [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + break; + default: // Automatic + if (@available(macOS 10.14, *)) { + // Use content background for lighter automatic effect + [effectView setMaterial:NSVisualEffectMaterialContentBackground]; + } else { + [effectView setMaterial:NSVisualEffectMaterialAppearanceBased]; + } + [effectView setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + break; + } + } + + // Use followsWindowActiveState for automatic adjustment + [effectView setState:NSVisualEffectStateFollowsWindowActiveState]; + + // Don't emphasize - it makes the effect too dark + if (@available(macOS 10.12, *)) { + [effectView setEmphasized:NO]; + } + + // Apply corner radius if specified + if (cornerRadius > 0) { + [effectView setWantsLayer:YES]; + effectView.layer.cornerRadius = cornerRadius; + effectView.layer.masksToBounds = YES; + } + } + + // Get the content view + NSView* contentView = [window contentView]; + + // Set up the glass view + [glassView setFrame:contentView.bounds]; + [glassView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + // Check if this is a real NSGlassEffectView with contentView property + BOOL hasContentView = [glassView respondsToSelector:@selector(contentView)]; + + if (hasContentView) { + // NSGlassEffectView: Add it to window and webView goes in its contentView + [contentView addSubview:glassView positioned:NSWindowBelow relativeTo:nil]; + + // Safely reparent the webView to the glass view's contentView + WKWebView* webView = window.webView; + NSView* glassContentView = [glassView valueForKey:@"contentView"]; + + // Only proceed if both webView and glassContentView are non-nil + if (webView && glassContentView) { + // Always remove from current superview to avoid exceptions + [webView removeFromSuperview]; + + // Add to the glass view's contentView + [glassContentView addSubview:webView]; + [webView setFrame:glassContentView.bounds]; + [webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + } + } else { + // NSVisualEffectView: Add glass as bottom layer, webView on top + [contentView addSubview:glassView positioned:NSWindowBelow relativeTo:nil]; + + WKWebView* webView = window.webView; + if (webView) { + [webView removeFromSuperview]; + [contentView addSubview:webView positioned:NSWindowAbove relativeTo:glassView]; + } + } + + // Configure WebView for liquid glass + configureWebViewForLiquidGlass(nsWindow); + + // Make window transparent + [window setOpaque:NO]; + [window setBackgroundColor:[NSColor clearColor]]; +} diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index 89be6c878..4c9c0f57a 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -383,6 +383,8 @@ const ( MacBackdropTransparent // MacBackdropTranslucent - The window will have a translucent background, with the content underneath it being "fuzzy" or "frosted" MacBackdropTranslucent + // MacBackdropLiquidGlass - The window will use Apple's Liquid Glass effect (macOS 15.0+ with fallback to translucent) + MacBackdropLiquidGlass ) // MacToolbarStyle is the style of toolbar for macOS @@ -401,6 +403,67 @@ const ( MacToolbarStyleUnifiedCompact ) +// MacLiquidGlassStyle defines the style of the Liquid Glass effect +type MacLiquidGlassStyle int + +const ( + // LiquidGlassStyleAutomatic - System determines the best style + LiquidGlassStyleAutomatic MacLiquidGlassStyle = iota + // LiquidGlassStyleLight - Light glass appearance + LiquidGlassStyleLight + // LiquidGlassStyleDark - Dark glass appearance + LiquidGlassStyleDark + // LiquidGlassStyleVibrant - Vibrant glass with enhanced effects + LiquidGlassStyleVibrant +) + +// NSVisualEffectMaterial represents the NSVisualEffectMaterial enum for macOS +type NSVisualEffectMaterial int + +const ( + // NSVisualEffectMaterial values from macOS SDK + NSVisualEffectMaterialAppearanceBased NSVisualEffectMaterial = 0 + NSVisualEffectMaterialLight NSVisualEffectMaterial = 1 + NSVisualEffectMaterialDark NSVisualEffectMaterial = 2 + NSVisualEffectMaterialTitlebar NSVisualEffectMaterial = 3 + NSVisualEffectMaterialSelection NSVisualEffectMaterial = 4 + NSVisualEffectMaterialMenu NSVisualEffectMaterial = 5 + NSVisualEffectMaterialPopover NSVisualEffectMaterial = 6 + NSVisualEffectMaterialSidebar NSVisualEffectMaterial = 7 + NSVisualEffectMaterialHeaderView NSVisualEffectMaterial = 10 + NSVisualEffectMaterialSheet NSVisualEffectMaterial = 11 + NSVisualEffectMaterialWindowBackground NSVisualEffectMaterial = 12 + NSVisualEffectMaterialHUDWindow NSVisualEffectMaterial = 13 + NSVisualEffectMaterialFullScreenUI NSVisualEffectMaterial = 15 + NSVisualEffectMaterialToolTip NSVisualEffectMaterial = 17 + NSVisualEffectMaterialContentBackground NSVisualEffectMaterial = 18 + NSVisualEffectMaterialUnderWindowBackground NSVisualEffectMaterial = 21 + NSVisualEffectMaterialUnderPageBackground NSVisualEffectMaterial = 22 + NSVisualEffectMaterialAuto NSVisualEffectMaterial = -1 // Use auto-selection based on Style +) + +// MacLiquidGlass contains configuration for the Liquid Glass effect +type MacLiquidGlass struct { + // Style of the glass effect + Style MacLiquidGlassStyle + + // Material to use for NSVisualEffectView (when NSGlassEffectView is not available) + // Set to NSVisualEffectMaterialAuto to use automatic selection based on Style + Material NSVisualEffectMaterial + + // Corner radius for the glass effect (0 for square corners) + CornerRadius float64 + + // Tint color for the glass (optional, nil for no tint) + TintColor *RGBA + + // Group identifier for merging multiple glass windows + GroupID string + + // Spacing between grouped glass elements (in points) + GroupSpacing float64 +} + // MacWindow contains macOS specific options for Webview Windows type MacWindow struct { // Backdrop is the backdrop type for the window @@ -425,6 +488,9 @@ type MacWindow struct { // WindowLevel sets the window level to control the order of windows in the screen WindowLevel MacWindowLevel + + // LiquidGlass contains configuration for the Liquid Glass effect + LiquidGlass MacLiquidGlass } type MacWindowLevel string