From 4bfc52f0b5954f82167d5c467bb0903a9cea9ca5 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 24 Aug 2025 07:16:19 +1000 Subject: [PATCH] Add native Liquid Glass effect support for macOS (#4534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement native Liquid Glass effect for macOS feat: Add platform check for Liquid Glass demo Show informative dialog on Windows/Linux explaining that the Liquid Glass effect is a macOS-specific feature. The demo will exit gracefully on non-macOS platforms. docs: Add Liquid Glass feature to unreleased changelog feat: Enhanced Liquid Glass effect with NSVisualEffectMaterial support Major improvements to the Liquid Glass implementation for macOS: - Added comprehensive NSVisualEffectMaterial support with 15+ material options - Removed debug NSLog statements for cleaner production code - Created multi-window demo showcasing 7 different glass effects: * Light Style - Clean light appearance * Dark Style - Dark themed glass * Vibrant Style - Enhanced transparency * Blue Tint - Custom RGBA tint color example * Sheet Material - NSVisualEffectMaterialSheet * HUD Window - Ultra-light HUD material * Content Background - With warm tint color - Added Material field to MacLiquidGlass struct for fine-grained control - Improved demo design with proper Title Case and cleaner layout - Fixed logo sizing to prevent blur - All windows fully draggable with InvisibleTitleBarHeight - Added comprehensive README documentation The implementation now provides developers with complete control over the glass effect appearance, supporting both native NSGlassEffectView (macOS 15.0+) and NSVisualEffectView fallback for older systems. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude feat: Implement native Liquid Glass effect for macOS - Add support for NSGlassEffectView on macOS 15.0+ - Implement runtime detection of native glass APIs - Add fallback to enhanced NSVisualEffectView for older systems - Update liquid glass demo with frameless windows for better visibility - Support all NSGlassEffectView properties (cornerRadius, tintColor, style) - Properly handle webview layering with glass effect - Remove binary from version control 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: Address CodeRabbit review feedback - Comment out unimplemented ReduceMotion and StaticMode fields - Remove overly broad draggable CSS properties - Add corner radius validation - Improve CSS with proper pointer-events and user-select - Add clarifying comments about future features * fix: Remove unimplemented ReduceMotion and StaticMode fields Completely remove the commented-out performance optimization fields as they are not implemented and have no timeline for implementation. * fix: Update windowRemoveVisualEffects to also remove NSGlassEffectView instances The cleanup function now properly removes both NSVisualEffectView and NSGlassEffectView instances to prevent orphaned effect layers. Uses NSClassFromString to avoid hard references to NSGlassEffectView which is only available on macOS 15.0+. * fix changelog * Update v3/pkg/application/webview_window_darwin.m Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update v3/pkg/application/webview_window_darwin.m Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: Fix compilation errors in windowSetLiquidGlass - Add missing NSGlassEffectViewStyle enum definition - Fix undefined tintColor variable by creating NSColor before use - Add autorelease to prevent memory leaks for allocated views These issues were causing CI build failures while working locally due to different compiler settings. * Update Taskfile.yaml * feat: Implement groupID and groupSpacing for NSGlassEffectView - Add runtime detection for groupIdentifier/groupName selectors - Apply groupID via performSelector if supported - Apply groupSpacing via KVC if supported - Parameters are now functional when NSGlassEffectView supports them - Maintains backward compatibility by checking selector availability * test: Add liquid-glass example to test suite - Add liquid-glass to EXAMPLEDIRS in Taskfile.yaml - Ensures the example is tested during CI builds - Validates compilation on different platforms Addresses review comment about missing test coverage * fix: Correct NSGlassEffectView availability to macOS 26.0 - Update @available checks from macOS 15.0 to 26.0 for NSGlassEffectView - NSGlassEffectView is a private API introduced in macOS 26.0 - Update README to reflect correct version requirement - Keep NSVisualEffectMaterial checks at 15.0 as those are different APIs * fix: Prevent exceptions from unsafe WebView reparenting - Remove early WebView addition to glassView.contentView - Consolidate all WebView reparenting in one safe location - Always call removeFromSuperview before adding to new parent - Set frame and autoresizing mask after safe reparenting - Prevents NSInternalInconsistencyException from multiple parents * fix: Make WebView reparenting more robust and thread-safe - Always call removeFromSuperview before adding to new parent - Remove brittle superview check, always detach and reattach - Check both webView and glassContentView are non-nil before operations - Ensure all UI operations run on main thread with dispatch_sync - Set frame and autoresizing mask after safe reparenting * Tidy up * Update changelog --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- v3/Taskfile.yaml | 2 + v3/UNRELEASED_CHANGELOG.md | 1 + v3/examples/liquid-glass/.gitignore | 1 + v3/examples/liquid-glass/README.md | 70 ++++++ v3/examples/liquid-glass/index.html | 50 ++++ v3/examples/liquid-glass/main.go | 235 ++++++++++++++++++ v3/examples/liquid-glass/wails-logo.png | Bin 0 -> 3842 bytes v3/pkg/application/webview_window_darwin.go | 53 +++- v3/pkg/application/webview_window_darwin.h | 8 + v3/pkg/application/webview_window_darwin.m | 248 ++++++++++++++++++- v3/pkg/application/webview_window_options.go | 66 +++++ 11 files changed, 725 insertions(+), 9 deletions(-) create mode 100644 v3/examples/liquid-glass/.gitignore create mode 100644 v3/examples/liquid-glass/README.md create mode 100644 v3/examples/liquid-glass/index.html create mode 100644 v3/examples/liquid-glass/main.go create mode 100644 v3/examples/liquid-glass/wails-logo.png 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 0000000000000000000000000000000000000000..e65c582ff27b00003010fddd39c9a3bb52a1422a GIT binary patch literal 3842 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 #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