Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
67 changes: 66 additions & 1 deletion Sources/Luminare/Components/Auxiliary/VisualEffectView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
15 changes: 15 additions & 0 deletions Sources/Luminare/Main Window/LuminareView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public struct LuminareView<Content>: View where Content: View {
// MARK: Environments

@Environment(\.luminareTintColor) private var tintColor
@Environment(\.luminareBackgroundBlurStyle) private var backgroundBlurStyle

// MARK: Fields

Expand All @@ -32,5 +33,19 @@ public struct LuminareView<Content>: 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)
}
}
}
}
13 changes: 10 additions & 3 deletions Sources/Luminare/Main Window/LuminareWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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
Expand Down Expand Up @@ -66,7 +73,7 @@ public class LuminareWindow: NSWindow {
}

func relocateTrafficLights() {
guard let contentView, let trafficLightsOrigin else {
guard contentView != nil, let trafficLightsOrigin else {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SwiftUI

extension EnvironmentValues {
@Entry var luminareClickedOutside: Bool = false
@Entry var luminareBackgroundBlurStyle: LuminareBackgroundBlurStyle = .regular
}

// MARK: - Common
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 18 additions & 0 deletions Sources/Luminare/Utilities/LuminareBackgroundBlurStyle.swift
Original file line number Diff line number Diff line change
@@ -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)
}
Loading