Update LiquidGlassBackground to remove polling

This commit is contained in:
Hariharan Mudaliar 2026-02-22 13:34:03 +05:30
commit 1d6cdf866a
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
8 changed files with 145 additions and 59 deletions

View file

@ -132,11 +132,9 @@ struct ExtensionLockScreenWidgetView: View {
@ViewBuilder @ViewBuilder
private func liquidGlassBackground(shape: RoundedRectangle) -> some View { private func liquidGlassBackground(shape: RoundedRectangle) -> some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius) {
LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
Color.black.opacity(0.08) Color.black.opacity(0.08)
} }
}
.overlay { .overlay {
tintOverlay(shape: shape) tintOverlay(shape: shape)
} }

View file

@ -792,11 +792,9 @@ struct LockScreenMusicPanel: View {
@ViewBuilder @ViewBuilder
private var customLiquidPanelBackdrop: some View { private var customLiquidPanelBackdrop: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius) {
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
Color.white.opacity(0.04) Color.white.opacity(0.04)
} }
}
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false) .allowsHitTesting(false)
.accessibilityHidden(true) .accessibilityHidden(true)
@ -873,12 +871,10 @@ struct LockScreenMusicPanel: View {
@ViewBuilder @ViewBuilder
private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View { private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius) {
LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
Color.clear Color.clear
} }
} }
}
private func logGlassState(reason: String) { private func logGlassState(reason: String) {
let snapshot = GlassLogSnapshot( let snapshot = GlassLogSnapshot(

View file

@ -154,12 +154,10 @@ struct LockScreenTimerWidget: View {
} }
private var customLiquidBackground: some View { private var customLiquidBackground: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius) {
LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) {
Color.black.opacity(0.12) Color.black.opacity(0.12)
} }
} }
}
private var frostedBackground: some View { private var frostedBackground: some View {
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)

View file

@ -163,15 +163,12 @@ struct CustomOSDView: View {
case .liquid: case .liquid:
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
if osdLiquidGlassCustomizationMode == .customLiquid { if osdLiquidGlassCustomizationMode == .customLiquid {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground( LiquidGlassBackground(
variant: osdLiquidGlassVariant, variant: osdLiquidGlassVariant,
cornerRadius: 18, cornerRadius: 18
trigger: context.date.timeIntervalSinceReferenceDate
) { ) {
Color.white.opacity(0.04) Color.white.opacity(0.04)
} }
}
} else { } else {
RoundedRectangle(cornerRadius: 18, style: .continuous) RoundedRectangle(cornerRadius: 18, style: .continuous)
.glassEffect( .glassEffect(

View file

@ -265,15 +265,12 @@ struct VerticalHUDView: View {
case .liquid: case .liquid:
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
if verticalHUDLiquidGlassCustomizationMode == .customLiquid { if verticalHUDLiquidGlassCustomizationMode == .customLiquid {
TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in
LiquidGlassBackground( LiquidGlassBackground(
variant: verticalHUDLiquidGlassVariant, variant: verticalHUDLiquidGlassVariant,
cornerRadius: max(currentWidth, hudWidth), cornerRadius: max(currentWidth, hudWidth)
trigger: context.date.timeIntervalSinceReferenceDate
) { ) {
Color.white.opacity(0.04) Color.white.opacity(0.04)
} }
}
} else { } else {
Capsule() Capsule()
.fill(.clear) .fill(.clear)

View file

@ -4520,7 +4520,6 @@ private struct LockScreenGlassVariantPreviewCell: View {
private let cornerRadius: CGFloat = 16 private let cornerRadius: CGFloat = 16
private let previewCornerRadius: CGFloat = 14 private let previewCornerRadius: CGFloat = 14
private let previewSize = CGSize(width: 190, height: 96) private let previewSize = CGSize(width: 190, height: 96)
@State private var previewToken: Double = 0
var body: some View { var body: some View {
ZStack { ZStack {
@ -4549,7 +4548,6 @@ private struct LockScreenGlassVariantPreviewCell: View {
Logger.log("Lock screen glass preview disappeared", category: .performance) Logger.log("Lock screen glass preview disappeared", category: .performance)
} }
.onChange(of: variant) { _, newValue in .onChange(of: variant) { _, newValue in
previewToken = Date().timeIntervalSinceReferenceDate
Logger.log("Lock screen glass preview variant changed to v\(newValue.rawValue)", category: .performance) Logger.log("Lock screen glass preview variant changed to v\(newValue.rawValue)", category: .performance)
} }
} }
@ -4557,9 +4555,7 @@ private struct LockScreenGlassVariantPreviewCell: View {
private var liquidGlassPreview: some View { private var liquidGlassPreview: some View {
LiquidGlassBackground( LiquidGlassBackground(
variant: variant, variant: variant,
cornerRadius: previewCornerRadius, cornerRadius: previewCornerRadius
trigger: previewToken,
jitterEnabled: false
) { ) {
Color.white.opacity(0.04) Color.white.opacity(0.04)
} }

View file

@ -59,9 +59,10 @@ class SettingsWindowController: NSWindowController {
window.titleVisibility = .visible window.titleVisibility = .visible
window.toolbarStyle = .unified window.toolbarStyle = .unified
window.isMovableByWindowBackground = true window.isMovableByWindowBackground = true
window.level = .normal
// Make it behave like a regular app window with proper Spaces support // Make it behave like a regular app window with proper Spaces support
window.collectionBehavior = [.managed, .participatesInCycle, .fullScreenAuxiliary] window.collectionBehavior = [.managed, .participatesInCycle]
// Ensure proper window behavior // Ensure proper window behavior
window.hidesOnDeactivate = false window.hidesOnDeactivate = false
@ -86,6 +87,10 @@ class SettingsWindowController: NSWindowController {
// Ensure window exists // Ensure window exists
_ = window _ = window
// Reassert regular window semantics in case any prior state mutated this window.
window?.level = .normal
window?.collectionBehavior = [.managed, .participatesInCycle]
// If window is already visible, bring it to front properly // If window is already visible, bring it to front properly
if window?.isVisible == true { if window?.isVisible == true {
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)

View file

@ -20,6 +20,98 @@ import SwiftUI
import AppKit import AppKit
import Defaults import Defaults
private final class LiquidGlassContainerView: NSView {
weak var glassView: NSView?
var hostingView: NSHostingView<AnyView>?
private var observedBackdropLayers: [CALayer] = []
private var hasScheduledBackdropSetup = false
deinit {
removeBackdropObservers()
}
override func removeFromSuperview() {
removeBackdropObservers()
super.removeFromSuperview()
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
scheduleBackdropSetup()
}
override func layout() {
super.layout()
scheduleBackdropSetup()
}
func scheduleBackdropSetup() {
guard !hasScheduledBackdropSetup else { return }
hasScheduledBackdropSetup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
guard let self else { return }
self.hasScheduledBackdropSetup = false
self.configureBackdropLayers()
}
}
private func configureBackdropLayers() {
guard let glassView else { return }
guard let rootLayer = glassView.layer else {
scheduleBackdropSetup()
return
}
setBackdropProperties(in: rootLayer)
let newBackdropLayers = collectBackdropLayers(in: rootLayer)
removeBackdropObservers()
observedBackdropLayers = newBackdropLayers
for backdrop in observedBackdropLayers {
backdrop.addObserver(self, forKeyPath: "windowServerAware", options: [.old, .new], context: nil)
}
}
private func setBackdropProperties(in layer: CALayer) {
if NSStringFromClass(type(of: layer)).contains("CABackdropLayer") {
layer.setValue(true, forKey: "windowServerAware")
}
layer.sublayers?.forEach { setBackdropProperties(in: $0) }
}
private func collectBackdropLayers(in layer: CALayer) -> [CALayer] {
var results: [CALayer] = []
if NSStringFromClass(type(of: layer)).contains("CABackdropLayer") {
results.append(layer)
}
layer.sublayers?.forEach { results.append(contentsOf: collectBackdropLayers(in: $0)) }
return results
}
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
if keyPath == "windowServerAware" {
if change?[.newKey] as? Bool == false {
configureBackdropLayers()
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
private func removeBackdropObservers() {
for layer in observedBackdropLayers {
layer.removeObserver(self, forKeyPath: "windowServerAware")
}
observedBackdropLayers.removeAll()
}
}
/// All 20 available liquid‑glass variants. /// All 20 available liquid‑glass variants.
/// Apple does not publicly describe how each value looks so experiment and pick the one you like! /// Apple does not publicly describe how each value looks so experiment and pick the one you like!
public enum LiquidGlassVariant: Int, CaseIterable, Identifiable, Defaults.Serializable, Sendable { public enum LiquidGlassVariant: Int, CaseIterable, Identifiable, Defaults.Serializable, Sendable {
@ -57,8 +149,6 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
private let content: Content private let content: Content
private let cornerRadius: CGFloat private let cornerRadius: CGFloat
private let variant: LiquidGlassVariant private let variant: LiquidGlassVariant
private let trigger: Double
private let jitterEnabled: Bool
/// Creates a new liquid‑glass container. /// Creates a new liquid‑glass container.
/// - Parameters: /// - Parameters:
/// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing /// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing
@ -67,14 +157,10 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
public init( public init(
variant: LiquidGlassVariant = .defaultVariant, variant: LiquidGlassVariant = .defaultVariant,
cornerRadius: CGFloat = 10, cornerRadius: CGFloat = 10,
trigger: Double = 0,
jitterEnabled: Bool = true,
@ViewBuilder content: () -> Content @ViewBuilder content: () -> Content
) { ) {
self.variant = variant self.variant = variant
self.cornerRadius = cornerRadius self.cornerRadius = cornerRadius
self.trigger = trigger
self.jitterEnabled = jitterEnabled
self.content = content() self.content = content()
} }
@ -116,14 +202,30 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
public func makeNSView(context: Context) -> NSView { public func makeNSView(context: Context) -> NSView {
// `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling. // `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling.
if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type { if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type {
let container = LiquidGlassContainerView(frame: .zero)
container.translatesAutoresizingMaskIntoConstraints = false
let glass = glassType.init(frame: .zero) let glass = glassType.init(frame: .zero)
glass.translatesAutoresizingMaskIntoConstraints = false
glass.setValue(cornerRadius, forKey: "cornerRadius") glass.setValue(cornerRadius, forKey: "cornerRadius")
callPrivateVariantSetter(on: glass, value: variant.rawValue) callPrivateVariantSetter(on: glass, value: variant.rawValue)
let hosting = NSHostingView(rootView: content) let hosting = NSHostingView(rootView: AnyView(content))
hosting.translatesAutoresizingMaskIntoConstraints = false hosting.translatesAutoresizingMaskIntoConstraints = false
glass.setValue(hosting, forKey: "contentView") glass.setValue(hosting, forKey: "contentView")
return glass
container.addSubview(glass)
NSLayoutConstraint.activate([
glass.leadingAnchor.constraint(equalTo: container.leadingAnchor),
glass.trailingAnchor.constraint(equalTo: container.trailingAnchor),
glass.topAnchor.constraint(equalTo: container.topAnchor),
glass.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
container.glassView = glass
container.hostingView = hosting
container.scheduleBackdropSetup()
return container
} }
// Fallback for earlier macOS – use an ordinary blur. // Fallback for earlier macOS – use an ordinary blur.
@ -143,20 +245,17 @@ public struct LiquidGlassBackground<Content: View>: NSViewRepresentable {
} }
public func updateNSView(_ nsView: NSView, context: Context) { public func updateNSView(_ nsView: NSView, context: Context) {
if let hosting = nsView.value(forKey: "contentView") as? NSHostingView<Content> { if let container = nsView as? LiquidGlassContainerView,
let glass = container.glassView {
container.hostingView?.rootView = AnyView(content)
glass.setValue(cornerRadius, forKey: "cornerRadius")
callPrivateVariantSetter(on: glass, value: variant.rawValue)
container.scheduleBackdropSetup()
return
}
if let hosting = nsView.subviews.first as? NSHostingView<Content> {
hosting.rootView = content hosting.rootView = content
} }
nsView.setValue(cornerRadius, forKey: "cornerRadius")
callPrivateVariantSetter(on: nsView, value: variant.rawValue)
if jitterEnabled {
// Micro-jitter: imperceptibly adjust opacity so WindowServer re-samples the wallpaper every frame.
let jitter = sin(trigger * 100) * 0.000001
nsView.alphaValue = 1.0 - CGFloat(abs(jitter))
} else if nsView.alphaValue != 1.0 {
nsView.alphaValue = 1.0
}
nsView.needsDisplay = true
} }
} }