diff --git a/DynamicIsland/components/Extensions/ExtensionLockScreenWidgetView.swift b/DynamicIsland/components/Extensions/ExtensionLockScreenWidgetView.swift index fe823ca..6d0d38b 100644 --- a/DynamicIsland/components/Extensions/ExtensionLockScreenWidgetView.swift +++ b/DynamicIsland/components/Extensions/ExtensionLockScreenWidgetView.swift @@ -132,10 +132,8 @@ struct ExtensionLockScreenWidgetView: View { @ViewBuilder private func liquidGlassBackground(shape: RoundedRectangle) -> some View { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) { - Color.black.opacity(0.08) - } + LiquidGlassBackground(variant: requestedLiquidGlassVariant, cornerRadius: descriptor.cornerRadius) { + Color.black.opacity(0.08) } .overlay { tintOverlay(shape: shape) diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index 46d09a1..4d88285 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -792,10 +792,8 @@ struct LockScreenMusicPanel: View { @ViewBuilder private var customLiquidPanelBackdrop: some View { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) { - Color.white.opacity(0.04) - } + LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius) { + Color.white.opacity(0.04) } .frame(maxWidth: .infinity, maxHeight: .infinity) .allowsHitTesting(false) @@ -873,10 +871,8 @@ struct LockScreenMusicPanel: View { @ViewBuilder private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) { - Color.clear - } + LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius) { + Color.clear } } diff --git a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift index ee52a5a..e13289d 100644 --- a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift @@ -154,10 +154,8 @@ struct LockScreenTimerWidget: View { } private var customLiquidBackground: some View { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius, trigger: context.date.timeIntervalSinceReferenceDate) { - Color.black.opacity(0.12) - } + LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius) { + Color.black.opacity(0.12) } } diff --git a/DynamicIsland/components/OSD/CustomOSDView.swift b/DynamicIsland/components/OSD/CustomOSDView.swift index 645ae69..19ea3cc 100644 --- a/DynamicIsland/components/OSD/CustomOSDView.swift +++ b/DynamicIsland/components/OSD/CustomOSDView.swift @@ -163,14 +163,11 @@ struct CustomOSDView: View { case .liquid: if #available(macOS 26.0, *) { if osdLiquidGlassCustomizationMode == .customLiquid { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground( - variant: osdLiquidGlassVariant, - cornerRadius: 18, - trigger: context.date.timeIntervalSinceReferenceDate - ) { - Color.white.opacity(0.04) - } + LiquidGlassBackground( + variant: osdLiquidGlassVariant, + cornerRadius: 18 + ) { + Color.white.opacity(0.04) } } else { RoundedRectangle(cornerRadius: 18, style: .continuous) diff --git a/DynamicIsland/components/OSD/VerticalHUDView.swift b/DynamicIsland/components/OSD/VerticalHUDView.swift index eaa2e3b..e27e69c 100644 --- a/DynamicIsland/components/OSD/VerticalHUDView.swift +++ b/DynamicIsland/components/OSD/VerticalHUDView.swift @@ -265,14 +265,11 @@ struct VerticalHUDView: View { case .liquid: if #available(macOS 26.0, *) { if verticalHUDLiquidGlassCustomizationMode == .customLiquid { - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { context in - LiquidGlassBackground( - variant: verticalHUDLiquidGlassVariant, - cornerRadius: max(currentWidth, hudWidth), - trigger: context.date.timeIntervalSinceReferenceDate - ) { - Color.white.opacity(0.04) - } + LiquidGlassBackground( + variant: verticalHUDLiquidGlassVariant, + cornerRadius: max(currentWidth, hudWidth) + ) { + Color.white.opacity(0.04) } } else { Capsule() diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 0b3336e..cb80cb8 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -4520,7 +4520,6 @@ private struct LockScreenGlassVariantPreviewCell: View { private let cornerRadius: CGFloat = 16 private let previewCornerRadius: CGFloat = 14 private let previewSize = CGSize(width: 190, height: 96) - @State private var previewToken: Double = 0 var body: some View { ZStack { @@ -4549,7 +4548,6 @@ private struct LockScreenGlassVariantPreviewCell: View { Logger.log("Lock screen glass preview disappeared", category: .performance) } .onChange(of: variant) { _, newValue in - previewToken = Date().timeIntervalSinceReferenceDate 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 { LiquidGlassBackground( variant: variant, - cornerRadius: previewCornerRadius, - trigger: previewToken, - jitterEnabled: false + cornerRadius: previewCornerRadius ) { Color.white.opacity(0.04) } diff --git a/DynamicIsland/components/Settings/SettingsWindowController.swift b/DynamicIsland/components/Settings/SettingsWindowController.swift index a80b382..ca37d37 100644 --- a/DynamicIsland/components/Settings/SettingsWindowController.swift +++ b/DynamicIsland/components/Settings/SettingsWindowController.swift @@ -59,9 +59,10 @@ class SettingsWindowController: NSWindowController { window.titleVisibility = .visible window.toolbarStyle = .unified window.isMovableByWindowBackground = true + window.level = .normal // 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 window.hidesOnDeactivate = false @@ -85,6 +86,10 @@ class SettingsWindowController: NSWindowController { func showWindow() { // Ensure window exists _ = 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?.isVisible == true { diff --git a/DynamicIsland/helpers/LiquidGlassBackground.swift b/DynamicIsland/helpers/LiquidGlassBackground.swift index 2af27bb..b7fb299 100644 --- a/DynamicIsland/helpers/LiquidGlassBackground.swift +++ b/DynamicIsland/helpers/LiquidGlassBackground.swift @@ -20,6 +20,98 @@ import SwiftUI import AppKit import Defaults +private final class LiquidGlassContainerView: NSView { + weak var glassView: NSView? + var hostingView: NSHostingView? + + 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. /// 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 { @@ -57,8 +149,6 @@ public struct LiquidGlassBackground: NSViewRepresentable { private let content: Content private let cornerRadius: CGFloat private let variant: LiquidGlassVariant - private let trigger: Double - private let jitterEnabled: Bool /// Creates a new liquid‑glass container. /// - Parameters: /// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing @@ -67,14 +157,10 @@ public struct LiquidGlassBackground: NSViewRepresentable { public init( variant: LiquidGlassVariant = .defaultVariant, cornerRadius: CGFloat = 10, - trigger: Double = 0, - jitterEnabled: Bool = true, @ViewBuilder content: () -> Content ) { self.variant = variant self.cornerRadius = cornerRadius - self.trigger = trigger - self.jitterEnabled = jitterEnabled self.content = content() } @@ -116,14 +202,30 @@ public struct LiquidGlassBackground: NSViewRepresentable { public func makeNSView(context: Context) -> NSView { // `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling. if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type { + let container = LiquidGlassContainerView(frame: .zero) + container.translatesAutoresizingMaskIntoConstraints = false + let glass = glassType.init(frame: .zero) + glass.translatesAutoresizingMaskIntoConstraints = false glass.setValue(cornerRadius, forKey: "cornerRadius") callPrivateVariantSetter(on: glass, value: variant.rawValue) - let hosting = NSHostingView(rootView: content) + let hosting = NSHostingView(rootView: AnyView(content)) hosting.translatesAutoresizingMaskIntoConstraints = false 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. @@ -143,20 +245,17 @@ public struct LiquidGlassBackground: NSViewRepresentable { } public func updateNSView(_ nsView: NSView, context: Context) { - if let hosting = nsView.value(forKey: "contentView") as? NSHostingView { + 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 { 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 } }