Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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()
}
Comment on lines +19 to +26

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When blurStyle is .custom, luminareBackground() now renders no background at all (the entire ZStack becomes empty). Since the PR description says .luminareBackground() pulls the style from the environment, it should also render a blur for .custom (likely by passing blurStyle into VisualEffectView) rather than becoming a no-op.

Copilot uses AI. Check for mistakes.
}
.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")
}
}
Comment on lines +41 to +55

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateNSView calls applyCustomBlurIfNeeded each time, which schedules an async block and then performs a recursive subview/sublayer search to find the backdrop layer. This can become expensive during frequent SwiftUI updates. Consider caching the resolved backdrop layer in a Coordinator (or associating it with the view once found) and only re-applying the radius when it changes, to avoid repeated tree traversal and repeated async scheduling.

Copilot uses AI. Check for mistakes.

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(blurStyle:)`` 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
) {
Comment thread
omeriadon marked this conversation as resolved.
Outdated
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 {
Comment thread
omeriadon marked this conversation as resolved.
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 targetting.
Comment thread
omeriadon marked this conversation as resolved.
Outdated
case custom(radius: CGFloat)
}
Loading