diff --git a/Sources/Luminare/Components/Auxiliary/LuminareBackgroundTintOverlay.swift b/Sources/Luminare/Components/Auxiliary/LuminareBackgroundTintOverlay.swift new file mode 100644 index 00000000..1cf68f3a --- /dev/null +++ b/Sources/Luminare/Components/Auxiliary/LuminareBackgroundTintOverlay.swift @@ -0,0 +1,20 @@ +// +// LuminareBackgroundTintOverlay.swift +// Luminare +// +// Created by Adon Omeri on 2025-03-24. +// + +import SwiftUI + +/// The tint overlay applied on top of any `VisualEffectView` inside Luminare backgrounds. +struct LuminareBackgroundTintOverlay: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Rectangle() + .foregroundStyle(.tint) + .opacity(colorScheme == .light ? 0.025 : 0.1) + .blendMode(.multiply) + } +} diff --git a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareBackgroundEffectModifier.swift b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareBackgroundEffectModifier.swift index 24ae97b5..f210a883 100644 --- a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareBackgroundEffectModifier.swift +++ b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareBackgroundEffectModifier.swift @@ -10,20 +10,20 @@ import SwiftUI /// A background effect that matches ``Luminare``. public struct LuminareBackgroundEffectModifier: ViewModifier { @Environment(\.colorScheme) private var colorscheme + @Environment(\.luminareBackgroundBlurStyle) private var blurStyle public func body(content: Content) -> some View { content .background { ZStack { - VisualEffectView( - material: .menu, - blendingMode: .behindWindow - ) + if blurStyle == .regular { + VisualEffectView( + material: .menu, + blendingMode: .behindWindow + ) - Rectangle() - .foregroundStyle(.tint) - .opacity(colorscheme == .light ? 0.025 : 0.1) - .blendMode(.multiply) + LuminareBackgroundTintOverlay() + } } .compositingGroup() .ignoresSafeArea() diff --git a/Sources/Luminare/Components/Auxiliary/VisualEffectView.swift b/Sources/Luminare/Components/Auxiliary/VisualEffectView.swift index 52f5a9ef..3e516a53 100644 --- a/Sources/Luminare/Components/Auxiliary/VisualEffectView.swift +++ b/Sources/Luminare/Components/Auxiliary/VisualEffectView.swift @@ -7,17 +7,82 @@ import SwiftUI +/// Source: https://oskargroth.com/blog/reverse-engineering-nsvisualeffectview struct VisualEffectView: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode + let blurStyle: LuminareBackgroundBlurStyle + + init( + material: NSVisualEffectView.Material, + blendingMode: NSVisualEffectView.BlendingMode, + blurStyle: LuminareBackgroundBlurStyle = .regular + ) { + self.material = material + self.blendingMode = blendingMode + self.blurStyle = blurStyle + } func makeNSView(context _: Context) -> NSVisualEffectView { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode visualEffectView.isEmphasized = true + applyCustomBlurIfNeeded(to: visualEffectView) return visualEffectView } - func updateNSView(_: NSVisualEffectView, context _: Context) {} + func updateNSView(_ view: NSVisualEffectView, context _: Context) { + view.material = material + view.blendingMode = blendingMode + applyCustomBlurIfNeeded(to: view) + } + + private func applyCustomBlurIfNeeded(to view: NSVisualEffectView) { + guard case let .custom(radius) = blurStyle else { + return + } + + view.wantsLayer = true + + DispatchQueue.main.async { + guard let backdropLayer = backdropLayer(in: view) else { + return + } + + backdropLayer.setValue(radius, forKeyPath: "filters.gaussianBlur.inputRadius") + } + } + + private func backdropLayer(in view: NSView) -> CALayer? { + if let layer = backdropLayer(in: view.layer) { + return layer + } + + for subview in view.subviews { + if let layer = backdropLayer(in: subview) { + return layer + } + } + + return nil + } + + private func backdropLayer(in layer: CALayer?) -> CALayer? { + guard let layer else { + return nil + } + + if String(describing: type(of: layer)).contains("Backdrop") { + return layer + } + + for sublayer in layer.sublayers ?? [] { + if let backdropLayer = backdropLayer(in: sublayer) { + return backdropLayer + } + } + + return nil + } } diff --git a/Sources/Luminare/Main Window/LuminareView.swift b/Sources/Luminare/Main Window/LuminareView.swift index 3769580e..264e4f5f 100644 --- a/Sources/Luminare/Main Window/LuminareView.swift +++ b/Sources/Luminare/Main Window/LuminareView.swift @@ -16,6 +16,7 @@ public struct LuminareView: View where Content: View { // MARK: Environments @Environment(\.luminareTintColor) private var tintColor + @Environment(\.luminareBackgroundBlurStyle) private var backgroundBlurStyle // MARK: Fields @@ -32,5 +33,19 @@ public struct LuminareView: View where Content: View { .focusable(false) .buttonStyle(.luminare) .luminareTint(overridingWith: tintColor) + .background { + if case .custom = backgroundBlurStyle { + ZStack { + VisualEffectView( + material: .menu, + blendingMode: .behindWindow, + blurStyle: backgroundBlurStyle + ) + LuminareBackgroundTintOverlay() + } + .ignoresSafeArea() + .allowsHitTesting(false) + } + } } } diff --git a/Sources/Luminare/Main Window/LuminareWindow.swift b/Sources/Luminare/Main Window/LuminareWindow.swift index 6c47816a..f7ae4547 100644 --- a/Sources/Luminare/Main Window/LuminareWindow.swift +++ b/Sources/Luminare/Main Window/LuminareWindow.swift @@ -22,8 +22,12 @@ public class LuminareWindow: NSWindow { /// Initializes a ``LuminareWindow``. /// /// - Parameters: + /// - backgroundBlurStyle: the default blur style used by ``luminareBackground()`` within the window. /// - content: the content view of the window, wrapped in a ``LuminareView``. - public init(content: @escaping () -> some View) { + public init( + backgroundBlurStyle: LuminareBackgroundBlurStyle = .regular, + content: @escaping () -> some View + ) { super.init( contentRect: .zero, styleMask: [.titled, .fullSizeContentView, .closable], @@ -33,7 +37,10 @@ public class LuminareWindow: NSWindow { // Wrapping the NSHostingView in a parent NSView allows us to reposition the traffic lights, since NSHostingViews cannot have subviews directly. let view = NSView() - let luminareView = NSHostingView(rootView: LuminareView(content: content)) + let luminareView = NSHostingView( + rootView: LuminareView(content: content) + .environment(\.luminareBackgroundBlurStyle, backgroundBlurStyle) + ) view.addSubview(luminareView) luminareView.translatesAutoresizingMaskIntoConstraints = false @@ -66,7 +73,7 @@ public class LuminareWindow: NSWindow { } func relocateTrafficLights() { - guard let contentView, let trafficLightsOrigin else { + guard contentView != nil, let trafficLightsOrigin else { return } diff --git a/Sources/Luminare/Utilities/Extensions/EnvironmentValues+Extensions.swift b/Sources/Luminare/Utilities/Extensions/EnvironmentValues+Extensions.swift index bcfbc486..9ee3766f 100644 --- a/Sources/Luminare/Utilities/Extensions/EnvironmentValues+Extensions.swift +++ b/Sources/Luminare/Utilities/Extensions/EnvironmentValues+Extensions.swift @@ -11,6 +11,7 @@ import SwiftUI extension EnvironmentValues { @Entry var luminareClickedOutside: Bool = false + @Entry var luminareBackgroundBlurStyle: LuminareBackgroundBlurStyle = .regular } // MARK: - Common diff --git a/Sources/Luminare/Utilities/Extensions/View+Extensions.swift b/Sources/Luminare/Utilities/Extensions/View+Extensions.swift index 9b1b6060..2019d55f 100644 --- a/Sources/Luminare/Utilities/Extensions/View+Extensions.swift +++ b/Sources/Luminare/Utilities/Extensions/View+Extensions.swift @@ -189,6 +189,7 @@ public extension View { ) } + /// Applies the standard Luminare background effect using the blur style from the environment. func luminareBackground() -> some View { modifier( LuminareBackgroundEffectModifier() diff --git a/Sources/Luminare/Utilities/LuminareBackgroundBlurStyle.swift b/Sources/Luminare/Utilities/LuminareBackgroundBlurStyle.swift new file mode 100644 index 00000000..6e83f5e8 --- /dev/null +++ b/Sources/Luminare/Utilities/LuminareBackgroundBlurStyle.swift @@ -0,0 +1,18 @@ +// +// LuminareBackgroundBlurStyle.swift +// Luminare +// +// Created by Adon Omeri on 2025-03-23. +// + +import SwiftUI + +/// Controls how `luminareBackground` and the window’s root view render their blur. +public enum LuminareBackgroundBlurStyle: Equatable, Sendable { + /// Applies a regular window material to the window. + /// - Note: This type does not use private APIs and is stable. + case regular + /// Set a custom blur level for the window. + /// - Warning: This type uses private APIs and may break in a future OS. Test on all macOS versions you are targeting. + case custom(radius: CGFloat) +}