Merge pull request #182 from Ebullioscopic/ui/glasseffect

Fix: Glass Effect on Lock Screen widgets and the Menu Bar button in Settings View
This commit is contained in:
Hariharan Mudaliar 2025-12-19 23:43:25 +05:30 • committed by GitHub
commit 792e9af2b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 246 additions and 89 deletions

View file

@ -8068,6 +8068,10 @@
"comment" : "A toggle that enables or disables the use of HUDs.", "comment" : "A toggle that enables or disables the use of HUDs.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Enable media panel blur" : {
"comment" : "A toggle switch that enables or disables the blur effect on the media panel.",
"isCommentAutoGenerated" : true
},
"Enable music live activity" : { "Enable music live activity" : {
"localizations" : { "localizations" : {
"ar" : { "ar" : {
@ -10988,6 +10992,10 @@
}, },
"Lock Screen Integration" : { "Lock Screen Integration" : {
},
"Lock Screen Liquid Glass" : {
"comment" : "The title of the music panel on the lock screen.",
"isCommentAutoGenerated" : true
}, },
"Lock screen panel preview" : { "Lock screen panel preview" : {
"comment" : "A description of the content displayed in the lock screen panel.", "comment" : "A description of the content displayed in the lock screen panel.",
@ -14872,6 +14880,14 @@
} }
} }
}, },
"Only applies when Material is set to Frosted Glass." : {
"comment" : "A description of the media panel blur setting, visible only when Material is set to Frosted Glass.",
"isCommentAutoGenerated" : true
},
"Only available when Material is set to Frosted Glass." : {
"comment" : "A description of a feature that is only available when a specific setting is enabled.",
"isCommentAutoGenerated" : true
},
"Only download icon" : { "Only download icon" : {
"extractionState" : "stale", "extractionState" : "stale",
"localizations" : { "localizations" : {

View file

@ -9,6 +9,11 @@ import SwiftUI
import Defaults import Defaults
struct LockScreenMusicPanel: View { struct LockScreenMusicPanel: View {
private struct GlassLogSnapshot: Equatable {
let style: LockScreenGlassStyle
let usesLiquidGlass: Bool
}
static let collapsedSize = CGSize(width: 420, height: 180) static let collapsedSize = CGSize(width: 420, height: 180)
static let expandedSize = CGSize(width: 720, height: 340) static let expandedSize = CGSize(width: 720, height: 340)
@ -23,6 +28,7 @@ struct LockScreenMusicPanel: View {
@State private var isExpanded = false @State private var isExpanded = false
@State private var isVolumeSliderVisible = false @State private var isVolumeSliderVisible = false
@State private var collapseWorkItem: DispatchWorkItem? @State private var collapseWorkItem: DispatchWorkItem?
@State private var lastLoggedGlassSnapshot: GlassLogSnapshot?
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle @Default(.lockScreenGlassStyle) var lockScreenGlassStyle
@Default(.lockScreenShowAppIcon) var showAppIcon @Default(.lockScreenShowAppIcon) var showAppIcon
@Default(.lockScreenPanelShowsBorder) var showPanelBorder @Default(.lockScreenPanelShowsBorder) var showPanelBorder
@ -48,6 +54,10 @@ struct LockScreenMusicPanel: View {
private let collapsedLyricsExtraHeight: CGFloat = 64 private let collapsedLyricsExtraHeight: CGFloat = 64
private let expandedLyricsExtraHeight: CGFloat = 96 private let expandedLyricsExtraHeight: CGFloat = 96
private var shouldUseFrostedBlur: Bool {
enableBlur && !usesLiquidGlass
}
private var currentSize: CGSize { private var currentSize: CGSize {
let base = isExpanded ? Self.expandedSize : Self.collapsedSize let base = isExpanded ? Self.expandedSize : Self.collapsedSize
return CGSize(width: base.width, height: base.height + totalExtraHeight) return CGSize(width: base.width, height: base.height + totalExtraHeight)
@ -74,45 +84,61 @@ struct LockScreenMusicPanel: View {
} }
private var panelContent: some View { private var panelContent: some View {
panelCore ZStack(alignment: .topLeading) {
.frame(width: currentSize.width, height: currentSize.height) panelBackgroundLayer
.animation(.spring(response: 0.48, dampingFraction: 0.82, blendDuration: 0.18), value: isExpanded) panelForeground
.animation(.spring(response: 0.45, dampingFraction: 0.85, blendDuration: 0.18), value: shouldShowVolumeSlider) }
.onAppear { .frame(width: currentSize.width, height: currentSize.height, alignment: .topLeading)
sliderValue = musicManager.elapsedTime .clipShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
isActive = true .overlay {
logPanelAppearance() if showPanelBorder && !usesLiquidGlass {
updatePanelSize(animated: false) RoundedRectangle(cornerRadius: panelCornerRadius)
routeManager.refreshDevices() .stroke(Color.white.opacity(0.35), lineWidth: 1.4)
} }
.onDisappear { }
isActive = false .shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
cancelCollapseTimer() .contentShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
isVolumeSliderVisible = false .animation(.spring(response: 0.48, dampingFraction: 0.82, blendDuration: 0.18), value: isExpanded)
} .animation(.spring(response: 0.45, dampingFraction: 0.85, blendDuration: 0.18), value: shouldShowVolumeSlider)
.onChange(of: isExpanded) { _, expanded in .onAppear {
updatePanelSize() sliderValue = musicManager.elapsedTime
} isActive = true
.onChange(of: showMediaOutputControl) { _, enabled in logPanelAppearance()
if !enabled { updatePanelSize(animated: false)
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) { routeManager.refreshDevices()
isVolumeSliderVisible = false logGlassState(reason: "Panel appeared")
} }
} .onDisappear {
updatePanelSize() isActive = false
} cancelCollapseTimer()
.onChange(of: enableLyrics) { _, _ in isVolumeSliderVisible = false
}
.onChange(of: isExpanded) { _, expanded in
updatePanelSize()
}
.onChange(of: showMediaOutputControl) { _, enabled in
if !enabled {
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) { withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) {
updatePanelSize() isVolumeSliderVisible = false
} }
} }
.scaleEffect(animator.isPresented ? 1 : 0.9, anchor: .center) updatePanelSize()
.opacity(animator.isPresented ? 1 : 0) }
.animation(.spring(response: 0.52, dampingFraction: 0.8), value: animator.isPresented) .onChange(of: enableLyrics) { _, _ in
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) {
updatePanelSize()
}
}
.onChange(of: lockScreenGlassStyle) { _, _ in
logGlassState(reason: "Glass style updated")
}
.scaleEffect(animator.isPresented ? 1 : 0.9, anchor: .center)
.opacity(animator.isPresented ? 1 : 0)
.animation(.spring(response: 0.52, dampingFraction: 0.8), value: animator.isPresented)
} }
@ViewBuilder @ViewBuilder
private var panelCore: some View { private var panelForeground: some View {
Group { Group {
if isExpanded { if isExpanded {
expandedLayout expandedLayout
@ -122,17 +148,7 @@ struct LockScreenMusicPanel: View {
} }
.padding(.horizontal, isExpanded ? 24 : 20) .padding(.horizontal, isExpanded ? 24 : 20)
.padding(.vertical, isExpanded ? 22 : 16) .padding(.vertical, isExpanded ? 22 : 16)
.frame(width: currentSize.width, height: currentSize.height, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(panelBackground)
.clipShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
.overlay {
if showPanelBorder {
RoundedRectangle(cornerRadius: panelCornerRadius)
.stroke(Color.white.opacity(0.35), lineWidth: 1.4)
}
}
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
.contentShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
} }
private var collapsedLayout: some View { private var collapsedLayout: some View {
@ -601,10 +617,17 @@ struct LockScreenMusicPanel: View {
.padding(.horizontal, 12) .padding(.horizontal, 12)
.background( .background(
RoundedRectangle(cornerRadius: isExpanded ? 16 : 12, style: .continuous) RoundedRectangle(cornerRadius: isExpanded ? 16 : 12, style: .continuous)
.fill(Color.white.opacity(0.08)) .fill(sliderBackgroundFill)
) )
} }
private var sliderBackgroundFill: Color {
if usesLiquidGlass {
return Color.white.opacity(0.05)
}
return Color.white.opacity(0.08)
}
private var shouldShowVolumeSlider: Bool { private var shouldShowVolumeSlider: Bool {
showMediaOutputControl && isVolumeSliderVisible showMediaOutputControl && isVolumeSliderVisible
} }
@ -679,35 +702,40 @@ struct LockScreenMusicPanel: View {
} }
@ViewBuilder @ViewBuilder
private var panelBackground: some View { private var panelBackgroundLayer: some View {
if enableBlur { if usesLiquidGlass {
if usesLiquidGlass { liquidPanelBackdrop
liquidPanelBackground } else if shouldUseFrostedBlur {
} else { frostedPanelBackground
frostedPanelBackground
}
} else { } else {
RoundedRectangle(cornerRadius: panelCornerRadius) opaquePanelBackground
.fill(Color.black.opacity(0.45))
} }
} }
@ViewBuilder @ViewBuilder
private var liquidPanelBackground: some View { private var liquidPanelBackdrop: some View {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
RoundedRectangle(cornerRadius: panelCornerRadius) GlassTextBackdrop(cornerRadius: panelCornerRadius)
.glassEffect( .frame(maxWidth: .infinity, maxHeight: .infinity)
.clear .allowsHitTesting(false)
//.tint(Color.white.opacity(0.12)) .accessibilityHidden(true)
.interactive(),
in: .rect(cornerRadius: panelCornerRadius)
)
} }
} }
private var frostedPanelBackground: some View { private var frostedPanelBackground: some View {
RoundedRectangle(cornerRadius: panelCornerRadius) RoundedRectangle(cornerRadius: panelCornerRadius)
.fill(.ultraThinMaterial) .fill(.ultraThinMaterial)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
private var opaquePanelBackground: some View {
RoundedRectangle(cornerRadius: panelCornerRadius)
.fill(Color.black.opacity(0.45))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
.accessibilityHidden(true)
} }
private func albumArtImage(size: CGFloat, cornerRadius: CGFloat) -> some View { private func albumArtImage(size: CGFloat, cornerRadius: CGFloat) -> some View {
@ -720,24 +748,13 @@ struct LockScreenMusicPanel: View {
@ViewBuilder @ViewBuilder
private func albumArtBackground(cornerRadius: CGFloat) -> some View { private func albumArtBackground(cornerRadius: CGFloat) -> some View {
if enableBlur { if usesLiquidGlass {
if usesLiquidGlass { if #available(macOS 26.0, *) {
if #available(macOS 26.0, *) { clearLiquidGlassSurface(cornerRadius: cornerRadius)
RoundedRectangle(cornerRadius: cornerRadius)
.glassEffect(
.clear
//.tint(Color.white.opacity(0.16))
.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.ultraThinMaterial)
}
} else {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.ultraThinMaterial)
} }
} else if shouldUseFrostedBlur {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.ultraThinMaterial)
} else { } else {
RoundedRectangle(cornerRadius: cornerRadius) RoundedRectangle(cornerRadius: cornerRadius)
.fill(Color.black.opacity(0.35)) .fill(Color.black.opacity(0.35))
@ -762,6 +779,50 @@ struct LockScreenMusicPanel: View {
isExpanded ? 18 : 12 isExpanded ? 18 : 12
} }
@available(macOS 26.0, *)
private func clearLiquidGlassSurface(cornerRadius: CGFloat) -> some View {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(.clear)
.glassEffect(
.clear.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
}
private func logGlassState(reason: String) {
let snapshot = GlassLogSnapshot(style: lockScreenGlassStyle, usesLiquidGlass: usesLiquidGlass)
guard snapshot != lastLoggedGlassSnapshot else { return }
lastLoggedGlassSnapshot = snapshot
struct ComponentState {
let name: String
let isLiquid: Bool
}
let states = [
ComponentState(name: "Panel Shell", isLiquid: usesLiquidGlass),
ComponentState(name: "Control Capsules", isLiquid: usesLiquidGlass),
ComponentState(name: "Volume Slider", isLiquid: usesLiquidGlass),
ComponentState(name: "Album Art Plate", isLiquid: usesLiquidGlass)
]
let componentSummary = states.map { entry -> String in
let mode = entry.isLiquid ? "Liquid" : "Frosted"
return "\(entry.name)=\(mode)"
}.joined(separator: ", ")
print("[LockScreenMusicPanel] \(reason) – style=\(lockScreenGlassStyle.rawValue), usesLiquidGlass=\(usesLiquidGlass), components[\(componentSummary)], macOS \(currentOSVersionDescription())")
if lockScreenGlassStyle == .liquid && !usesLiquidGlass {
print("[LockScreenMusicPanel] Liquid Glass requested but unavailable on this macOS build. Falling back to frosted visuals.")
}
}
private func currentOSVersionDescription() -> String {
let version = ProcessInfo.processInfo.operatingSystemVersion
return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
}
private func logPanelAppearance(event: String = "✅ View appeared") { private func logPanelAppearance(event: String = "✅ View appeared") {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS" formatter.dateFormat = "HH:mm:ss.SSS"
@ -889,3 +950,25 @@ private struct PanelControlButton: View {
case counterClockwise case counterClockwise
} }
} }
@available(macOS 26.0, *)
private struct GlassTextBackdrop: View {
let cornerRadius: CGFloat
var body: some View {
GeometryReader { proxy in
let dynamicFontSize = max(min(proxy.size.width, proxy.size.height) / 8, 42)
Text("Lock Screen Liquid Glass")
.font(.system(size: dynamicFontSize, weight: .semibold, design: .rounded))
.foregroundStyle(Color.clear)
.frame(width: proxy.size.width, height: proxy.size.height)
.glassEffect(
.clear.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
}
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}

View file

@ -301,12 +301,7 @@ struct SettingsView: View {
} }
.navigationSplitViewStyle(.balanced) .navigationSplitViewStyle(.balanced)
.toolbar(removing: .sidebarToggle) .toolbar(removing: .sidebarToggle)
.toolbar { .toolbar { toolbarSpacingShim }
Button("") {} // Empty label, does nothing
.controlSize(.extraLarge)
.opacity(0) // Invisible, but reserves space for a consistent look between tabs
.disabled(true)
}
.environmentObject(highlightCoordinator) .environmentObject(highlightCoordinator)
.formStyle(.grouped) .formStyle(.grouped)
.frame(width: 700) .frame(width: 700)
@ -343,6 +338,28 @@ struct SettingsView: View {
availableTabs.contains(selectedTab) ? selectedTab : (availableTabs.first ?? .general) availableTabs.contains(selectedTab) ? selectedTab : (availableTabs.first ?? .general)
} }
@ToolbarContentBuilder
private var toolbarSpacingShim: some ToolbarContent {
if #available(macOS 26.0, *) {
ToolbarItem(placement: .primaryAction) {
toolbarSpacerView
}
.sharedBackgroundVisibility(.hidden)
} else {
ToolbarItem(placement: .primaryAction) {
toolbarSpacerView
}
}
}
@ViewBuilder
private var toolbarSpacerView: some View {
Color.clear
.frame(width: 96, height: 32)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
private var filteredTabs: [SettingsTab] { private var filteredTabs: [SettingsTab] {
tabsMatchingSearch(searchText) tabsMatchingSearch(searchText)
} }
@ -1364,6 +1381,7 @@ struct Media: View {
@Default(.musicControlWindowEnabled) private var musicControlWindowEnabled @Default(.musicControlWindowEnabled) private var musicControlWindowEnabled
@Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
@Default(.showSneakPeekOnTrackChange) private var showSneakPeekOnTrackChange @Default(.showSneakPeekOnTrackChange) private var showSneakPeekOnTrackChange
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
private func highlightID(_ title: String) -> String { private func highlightID(_ title: String) -> String {
SettingsTab.media.highlightID(for: title) SettingsTab.media.highlightID(for: title)
@ -1493,8 +1511,15 @@ struct Media: View {
.disabled(!enableLockScreenMediaWidget) .disabled(!enableLockScreenMediaWidget)
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder) Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
.disabled(!enableLockScreenMediaWidget) .disabled(!enableLockScreenMediaWidget)
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur) if lockScreenGlassStyle == .frosted {
.disabled(!enableLockScreenMediaWidget) Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
.disabled(!enableLockScreenMediaWidget)
.settingsHighlight(id: highlightID("Enable media panel blur"))
} else {
unavailableBlurRow
.opacity(enableLockScreenMediaWidget ? 1 : 0.5)
.settingsHighlight(id: highlightID("Enable media panel blur"))
}
} header: { } header: {
Text("Lock Screen Integration") Text("Lock Screen Integration")
} footer: { } footer: {
@ -1525,6 +1550,17 @@ struct Media: View {
return MediaControllerType.allCases return MediaControllerType.allCases
} }
} }
private var unavailableBlurRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Enable media panel blur")
.foregroundStyle(.secondary)
Text("Only applies when Material is set to Frosted Glass.")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
struct CalendarSettings: View { struct CalendarSettings: View {
@ -2506,9 +2542,15 @@ struct LockScreenSettings: View {
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder) Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
.disabled(!enableLockScreenMediaWidget) .disabled(!enableLockScreenMediaWidget)
.settingsHighlight(id: highlightID("Show panel border")) .settingsHighlight(id: highlightID("Show panel border"))
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur) if lockScreenGlassStyle == .frosted {
.disabled(!enableLockScreenMediaWidget) Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
.settingsHighlight(id: highlightID("Enable media panel blur")) .disabled(!enableLockScreenMediaWidget)
.settingsHighlight(id: highlightID("Enable media panel blur"))
} else {
blurSettingUnavailableRow
.opacity(enableLockScreenMediaWidget ? 1 : 0.5)
.settingsHighlight(id: highlightID("Enable media panel blur"))
}
} header: { } header: {
Text("Media Panel") Text("Media Panel")
} footer: { } footer: {
@ -2633,6 +2675,19 @@ struct LockScreenSettings: View {
} }
} }
extension LockScreenSettings {
private var blurSettingUnavailableRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Enable media panel blur")
.foregroundStyle(.secondary)
Text("Only available when Material is set to Frosted Glass.")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct LockScreenPositioningControls: View { private struct LockScreenPositioningControls: View {
@Default(.lockScreenWeatherVerticalOffset) private var weatherOffset @Default(.lockScreenWeatherVerticalOffset) private var weatherOffset
@Default(.lockScreenMusicVerticalOffset) private var musicOffset @Default(.lockScreenMusicVerticalOffset) private var musicOffset

View file

@ -127,6 +127,7 @@ class LockScreenPanelManager {
content.wantsLayer = true content.wantsLayer = true
content.layer?.masksToBounds = true content.layer?.masksToBounds = true
content.layer?.cornerRadius = collapsedPanelCornerRadius content.layer?.cornerRadius = collapsedPanelCornerRadius
content.layer?.backgroundColor = NSColor.clear.cgColor
} }
if !hasDelegated { if !hasDelegated {

View file

@ -165,6 +165,8 @@ Atoll builds upon the work of several open-source projects and draws inspiration
- [**Open Meteo**](https://open-meteo.com) - weather apis for the lock screen widgets - [**Open Meteo**](https://open-meteo.com) - weather apis for the lock screen widgets
- [**SkyLightWindow**](https://github.com/Lakr233/SkyLightWindow) - window rendering for Lock Screen Widgets - [**SkyLightWindow**](https://github.com/Lakr233/SkyLightWindow) - window rendering for Lock Screen Widgets
- Wick - Thanks Nate for allowing us to replicate the iOS like Timer design for the Lock Screen Widget
## Contributors ## Contributors
<a href="https://github.com/Ebullioscopic/Atoll/graphs/contributors"> <a href="https://github.com/Ebullioscopic/Atoll/graphs/contributors">