Skip to content
Open
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
63 changes: 36 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,20 @@ jobs:
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
# Prefer Xcode 26 (macOS 26 SDK) so NSGlassEffectView / Liquid Glass compiles;
# fall back to the image default when no Xcode 26 install exists.
XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)"
if [ -z "$XCODE_APP" ] && [ -d "/Applications/Xcode.app" ]; then
XCODE_APP="/Applications/Xcode.app"
fi
if [ -z "$XCODE_APP" ]; then
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
if [ -z "$XCODE_APP" ]; then
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
XCODE_DIR="$XCODE_APP/Contents/Developer"
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
Expand Down Expand Up @@ -346,17 +349,20 @@ jobs:
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
# Prefer Xcode 26 (macOS 26 SDK) so NSGlassEffectView / Liquid Glass compiles;
# fall back to the image default when no Xcode 26 install exists.
XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)"
if [ -z "$XCODE_APP" ] && [ -d "/Applications/Xcode.app" ]; then
XCODE_APP="/Applications/Xcode.app"
fi
if [ -z "$XCODE_APP" ]; then
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
if [ -z "$XCODE_APP" ]; then
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
XCODE_DIR="$XCODE_APP/Contents/Developer"
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
Expand Down Expand Up @@ -519,17 +525,20 @@ jobs:
- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
# Prefer Xcode 26 (macOS 26 SDK) so NSGlassEffectView / Liquid Glass compiles;
# fall back to the image default when no Xcode 26 install exists.
XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)"
if [ -z "$XCODE_APP" ] && [ -d "/Applications/Xcode.app" ]; then
XCODE_APP="/Applications/Xcode.app"
fi
if [ -z "$XCODE_APP" ]; then
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
fi
if [ -z "$XCODE_APP" ]; then
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
XCODE_DIR="$XCODE_APP/Contents/Developer"
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
Expand Down
23 changes: 13 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,20 @@ jobs:
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
if [ -n "$XCODE_APP" ]; then
XCODE_DIR="$XCODE_APP/Contents/Developer"
else
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
# Prefer Xcode 26 (macOS 26 SDK) so NSGlassEffectView / Liquid Glass compiles;
# fall back to the image default when no Xcode 26 install exists.
XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)"
if [ -z "$XCODE_APP" ] && [ -d "/Applications/Xcode.app" ]; then
XCODE_APP="/Applications/Xcode.app"
fi
if [ -z "$XCODE_APP" ]; then
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
fi
if [ -z "$XCODE_APP" ]; then
echo "No Xcode.app found under /Applications" >&2
exit 1
fi
XCODE_DIR="$XCODE_APP/Contents/Developer"
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
Expand Down
2 changes: 1 addition & 1 deletion Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2327,7 +2327,7 @@ final class WindowBrowserPortal: HostedViewPortalRegistry {
private func installationTarget(for window: NSWindow) -> (container: NSView, reference: NSView)? {
guard let contentView = window.contentView else { return nil }

if contentView.className == "NSGlassEffectView",
if WindowGlassEffect.isGlassEffectView(contentView),
let foreground = contentView.subviews.first(where: { $0 !== hostView }) {
return (contentView, foreground)
}
Expand Down
160 changes: 86 additions & 74 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,30 @@ struct ShortcutHintPillBackground: View {
}
}

/// Applies NSGlassEffectView (macOS 26+) to a window, falling back to NSVisualEffectView
/// Applies NSGlassEffectView (macOS 26+) to a window, falling back to NSVisualEffectView.
/// The glass path requires both a macOS 26 SDK at build time and macOS 26 at runtime.
enum WindowGlassEffect {
private static var glassViewKey: UInt8 = 0
private static var originalContentViewKey: UInt8 = 0
private static var tintOverlayKey: UInt8 = 0

static var isAvailable: Bool {
NSClassFromString("NSGlassEffectView") != nil
#if compiler(>=6.2)
if #available(macOS 26.0, *) {
return true
}
#endif
return false
}

/// True when `view` is the native macOS 26 glass view.
static func isGlassEffectView(_ view: NSView) -> Bool {
#if compiler(>=6.2)
if #available(macOS 26.0, *) {
return view is NSGlassEffectView
}
#endif
return false
}

static func apply(to window: NSWindow, tintColor: NSColor? = nil) {
Expand All @@ -192,73 +208,72 @@ enum WindowGlassEffect {
return
}

let bounds = originalContentView.bounds

// Create the glass/blur view
let glassView: NSVisualEffectView
let usingGlassEffectView: Bool

// Try NSGlassEffectView first (macOS 26 Tahoe+)
if let glassClass = NSClassFromString("NSGlassEffectView") as? NSVisualEffectView.Type {
usingGlassEffectView = true
glassView = glassClass.init(frame: bounds)
glassView.wantsLayer = true
glassView.layer?.cornerRadius = 0

// Apply tint color via private API
if let color = tintColor {
let selector = NSSelectorFromString("setTintColor:")
if glassView.responds(to: selector) {
glassView.perform(selector, with: color)
}
}
} else {
usingGlassEffectView = false
// Fallback to NSVisualEffectView
glassView = NSVisualEffectView(frame: bounds)
glassView.blendingMode = .behindWindow
// Favor a lighter fallback so behind-window glass reads more transparent.
glassView.material = .underWindowBackground
glassView.state = .active
glassView.wantsLayer = true
#if compiler(>=6.2)
if #available(macOS 26.0, *) {
applyGlass(to: window, originalContentView: originalContentView, tintColor: tintColor)
return
}
#endif
applyVisualEffectFallback(to: window, originalContentView: originalContentView, tintColor: tintColor)
}

#if compiler(>=6.2)
@available(macOS 26.0, *)
private static func applyGlass(to window: NSWindow, originalContentView: NSView, tintColor: NSColor?) {
let glassView = NSGlassEffectView(frame: originalContentView.bounds)
glassView.wantsLayer = true
glassView.cornerRadius = 0
glassView.tintColor = tintColor
glassView.autoresizingMask = [.width, .height]

if usingGlassEffectView {
// NSGlassEffectView is a full replacement for the contentView.
objc_setAssociatedObject(window, &originalContentViewKey, originalContentView, .OBJC_ASSOCIATION_RETAIN)
window.contentView = glassView
// NSGlassEffectView is a full replacement for the contentView.
objc_setAssociatedObject(window, &originalContentViewKey, originalContentView, .OBJC_ASSOCIATION_RETAIN)
window.contentView = glassView

// Re-add the original SwiftUI hosting view on top of the glass, filling entire area.
originalContentView.translatesAutoresizingMaskIntoConstraints = false
originalContentView.wantsLayer = true
originalContentView.layer?.backgroundColor = NSColor.clear.cgColor
glassView.addSubview(originalContentView)
// Re-add the original SwiftUI hosting view on top of the glass, filling entire area.
// Kept as a manual subview (not NSGlassEffectView.contentView) so the window portal
// installation code can rely on the subview hierarchy.
originalContentView.translatesAutoresizingMaskIntoConstraints = false
originalContentView.wantsLayer = true
originalContentView.layer?.backgroundColor = NSColor.clear.cgColor
glassView.addSubview(originalContentView)

NSLayoutConstraint.activate([
originalContentView.topAnchor.constraint(equalTo: glassView.topAnchor),
originalContentView.bottomAnchor.constraint(equalTo: glassView.bottomAnchor),
originalContentView.leadingAnchor.constraint(equalTo: glassView.leadingAnchor),
originalContentView.trailingAnchor.constraint(equalTo: glassView.trailingAnchor)
])
} else {
// For NSVisualEffectView fallback (macOS 13-15), do NOT replace window.contentView.
// Replacing contentView can break traffic light rendering with
// `.fullSizeContentView` + `titlebarAppearsTransparent`.
glassView.translatesAutoresizingMaskIntoConstraints = false
originalContentView.addSubview(glassView, positioned: .below, relativeTo: nil)
NSLayoutConstraint.activate([
originalContentView.topAnchor.constraint(equalTo: glassView.topAnchor),
originalContentView.bottomAnchor.constraint(equalTo: glassView.bottomAnchor),
originalContentView.leadingAnchor.constraint(equalTo: glassView.leadingAnchor),
originalContentView.trailingAnchor.constraint(equalTo: glassView.trailingAnchor)
])

NSLayoutConstraint.activate([
glassView.topAnchor.constraint(equalTo: originalContentView.topAnchor),
glassView.bottomAnchor.constraint(equalTo: originalContentView.bottomAnchor),
glassView.leadingAnchor.constraint(equalTo: originalContentView.leadingAnchor),
glassView.trailingAnchor.constraint(equalTo: originalContentView.trailingAnchor)
])
}
objc_setAssociatedObject(window, &glassViewKey, glassView, .OBJC_ASSOCIATION_RETAIN)
}
#endif

private static func applyVisualEffectFallback(to window: NSWindow, originalContentView: NSView, tintColor: NSColor?) {
let bounds = originalContentView.bounds
let glassView = NSVisualEffectView(frame: bounds)
glassView.blendingMode = .behindWindow
// Favor a lighter fallback so behind-window glass reads more transparent.
glassView.material = .underWindowBackground
glassView.state = .active
glassView.wantsLayer = true
glassView.autoresizingMask = [.width, .height]

// For the NSVisualEffectView fallback, do NOT replace window.contentView.
// Replacing contentView can break traffic light rendering with
// `.fullSizeContentView` + `titlebarAppearsTransparent`.
glassView.translatesAutoresizingMaskIntoConstraints = false
originalContentView.addSubview(glassView, positioned: .below, relativeTo: nil)

NSLayoutConstraint.activate([
glassView.topAnchor.constraint(equalTo: originalContentView.topAnchor),
glassView.bottomAnchor.constraint(equalTo: originalContentView.bottomAnchor),
glassView.leadingAnchor.constraint(equalTo: originalContentView.leadingAnchor),
glassView.trailingAnchor.constraint(equalTo: originalContentView.trailingAnchor)
])

// Add tint overlay between glass and content (for fallback)
if let tintColor, !usingGlassEffectView {
// Add tint overlay between glass and content
if let tintColor {
let tintOverlay = NSView(frame: bounds)
tintOverlay.translatesAutoresizingMaskIntoConstraints = false
tintOverlay.wantsLayer = true
Expand All @@ -273,7 +288,6 @@ enum WindowGlassEffect {
objc_setAssociatedObject(window, &tintOverlayKey, tintOverlay, .OBJC_ASSOCIATION_RETAIN)
}

// Store reference
objc_setAssociatedObject(window, &glassViewKey, glassView, .OBJC_ASSOCIATION_RETAIN)
}

Expand All @@ -284,17 +298,15 @@ enum WindowGlassEffect {
}

private static func updateTint(on glassView: NSView, color: NSColor?, window: NSWindow) {
// For NSGlassEffectView, use setTintColor:
if glassView.className == "NSGlassEffectView" {
let selector = NSSelectorFromString("setTintColor:")
if glassView.responds(to: selector) {
glassView.perform(selector, with: color)
}
} else {
// For NSVisualEffectView fallback, update the tint overlay
if let tintOverlay = objc_getAssociatedObject(window, &tintOverlayKey) as? NSView {
tintOverlay.layer?.backgroundColor = color?.cgColor
}
#if compiler(>=6.2)
if #available(macOS 26.0, *), let glass = glassView as? NSGlassEffectView {
glass.tintColor = color
return
}
#endif
// For NSVisualEffectView fallback, update the tint overlay
if let tintOverlay = objc_getAssociatedObject(window, &tintOverlayKey) as? NSView {
tintOverlay.layer?.backgroundColor = color?.cgColor
}
}

Expand All @@ -303,7 +315,7 @@ enum WindowGlassEffect {
return
}

if glassView.className == "NSGlassEffectView" {
if isGlassEffectView(glassView) {
if let originalContentView = objc_getAssociatedObject(window, &originalContentViewKey) as? NSView {
originalContentView.removeFromSuperview()
originalContentView.translatesAutoresizingMaskIntoConstraints = true
Expand Down
35 changes: 15 additions & 20 deletions Sources/SidebarVisuals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1107,17 +1107,18 @@ private struct SidebarVisualEffectBackground: NSViewRepresentable {
}

static var liquidGlassAvailable: Bool {
NSClassFromString("NSGlassEffectView") != nil
WindowGlassEffect.isAvailable
}

func makeNSView(context: Context) -> NSView {
// Try NSGlassEffectView if preferred or if we want to test availability
if preferLiquidGlass, let glassClass = NSClassFromString("NSGlassEffectView") as? NSView.Type {
let glass = glassClass.init(frame: .zero)
#if compiler(>=6.2)
if preferLiquidGlass, #available(macOS 26.0, *) {
let glass = NSGlassEffectView(frame: .zero)
glass.autoresizingMask = [.width, .height]
glass.wantsLayer = true
return glass
}
#endif

// Use NSVisualEffectView
let view = NSVisualEffectView()
Expand All @@ -1128,22 +1129,16 @@ private struct SidebarVisualEffectBackground: NSViewRepresentable {
}

func updateNSView(_ nsView: NSView, context: Context) {
// Configure based on view type
if nsView.className == "NSGlassEffectView" {
// NSGlassEffectView configuration via private API
nsView.alphaValue = max(0.0, min(1.0, opacity))
nsView.layer?.cornerRadius = cornerRadius
nsView.layer?.masksToBounds = cornerRadius > 0

// Try to set tint color via private selector
if let color = tintColor {
let selector = NSSelectorFromString("setTintColor:")
if nsView.responds(to: selector) {
nsView.perform(selector, with: color)
}
}
} else if let visualEffect = nsView as? NSVisualEffectView {
// NSVisualEffectView configuration
#if compiler(>=6.2)
if #available(macOS 26.0, *), let glass = nsView as? NSGlassEffectView {
glass.alphaValue = max(0.0, min(1.0, opacity))
glass.cornerRadius = cornerRadius
glass.tintColor = tintColor
return
}
#endif

if let visualEffect = nsView as? NSVisualEffectView {
visualEffect.material = material
visualEffect.blendingMode = blendingMode
visualEffect.state = state
Expand Down
Loading
Loading