mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update LiquidGlassBackground to remove polling
This commit is contained in:
parent
4ec5c84b9c
commit
1d6cdf866a
8 changed files with 145 additions and 59 deletions
|
|
@ -132,10 +132,8 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -792,10 +792,8 @@ 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)
|
||||||
|
|
@ -873,10 +871,8 @@ 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,10 +154,8 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,14 +163,11 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -265,14 +265,11 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue