Update stuff for lock screen widget width

This commit is contained in:
Hariharan Mudaliar 2026-01-22 21:09:05 +05:30
commit ab6877e59c
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
7 changed files with 200 additions and 17 deletions

View file

@ -7493,8 +7493,9 @@
"comment" : "A description of how to interact with the music slot configuration view.", "comment" : "A description of how to interact with the music slot configuration view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Changes apply instantly while the widgets are visible." : { "Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Use the width sliders below to narrow the media and timer widgets without exceeding their default size. Changes apply instantly while the widgets are visible." : {
"comment" : "A footer text describing the functionality of the lock screen positioning controls.",
"isCommentAutoGenerated" : true
}, },
"Drop files here" : { "Drop files here" : {
"localizations" : { "localizations" : {

View file

@ -16,7 +16,11 @@ struct LockScreenMusicPanel: View {
let usesLiquidGlass: Bool let usesLiquidGlass: Bool
} }
static let collapsedSize = CGSize(width: 420, height: 180) static let collapsedHeight: CGFloat = 180
static let defaultCollapsedWidth: CGFloat = 420
static var collapsedSize: CGSize {
CGSize(width: CGFloat(Defaults[.lockScreenMusicPanelWidth]), height: collapsedHeight)
}
static let expandedSize = CGSize(width: 720, height: 340) static let expandedSize = CGSize(width: 720, height: 340)
@ObservedObject var musicManager = MusicManager.shared @ObservedObject var musicManager = MusicManager.shared
@ -45,6 +49,7 @@ struct LockScreenMusicPanel: View {
@Default(.musicSkipBehavior) private var musicSkipBehavior @Default(.musicSkipBehavior) private var musicSkipBehavior
@Default(.enableLyrics) private var enableLyrics @Default(.enableLyrics) private var enableLyrics
@Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenParallaxEnabled @Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenParallaxEnabled
@Default(.lockScreenMusicPanelWidth) private var collapsedPanelWidth
init(animator: LockScreenPanelAnimator) { init(animator: LockScreenPanelAnimator) {
_animator = ObservedObject(wrappedValue: animator) _animator = ObservedObject(wrappedValue: animator)
@ -66,10 +71,14 @@ struct LockScreenMusicPanel: View {
} }
private var currentSize: CGSize { private var currentSize: CGSize {
let base = isExpanded ? Self.expandedSize : Self.collapsedSize let base = isExpanded ? Self.expandedSize : collapsedPanelSize
return CGSize(width: base.width, height: base.height + totalExtraHeight) return CGSize(width: base.width, height: base.height + totalExtraHeight)
} }
private var collapsedPanelSize: CGSize {
CGSize(width: CGFloat(collapsedPanelWidth), height: Self.collapsedHeight)
}
private var panelCornerRadius: CGFloat { private var panelCornerRadius: CGFloat {
isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius
} }
@ -95,7 +104,7 @@ struct LockScreenMusicPanel: View {
panelContent panelContent
} else { } else {
Color.clear Color.clear
.frame(width: Self.collapsedSize.width, height: Self.collapsedSize.height) .frame(width: collapsedPanelSize.width, height: collapsedPanelSize.height)
} }
} }

View file

@ -2,7 +2,11 @@ import SwiftUI
import Defaults import Defaults
struct LockScreenTimerWidget: View { struct LockScreenTimerWidget: View {
static let preferredSize = CGSize(width: 420, height: 96) static let defaultWidth: Double = 420
static let preferredHeight: CGFloat = 96
static var preferredSize: CGSize {
CGSize(width: CGFloat(Defaults[.lockScreenTimerWidgetWidth]), height: preferredHeight)
}
static let cornerRadius: CGFloat = 26 static let cornerRadius: CGFloat = 26
@ObservedObject private var animator: LockScreenTimerWidgetAnimator @ObservedObject private var animator: LockScreenTimerWidgetAnimator
@ -12,6 +16,7 @@ struct LockScreenTimerWidget: View {
@Default(.lockScreenTimerLiquidGlassVariant) private var timerGlassVariant @Default(.lockScreenTimerLiquidGlassVariant) private var timerGlassVariant
@Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass
@Default(.timerPresets) private var timerPresets @Default(.timerPresets) private var timerPresets
@Default(.lockScreenTimerWidgetWidth) private var widgetWidth
@MainActor @MainActor
init(animator: LockScreenTimerWidgetAnimator? = nil) { init(animator: LockScreenTimerWidgetAnimator? = nil) {
@ -146,6 +151,10 @@ struct LockScreenTimerWidget: View {
.fill(Color.black.opacity(0.65)) .fill(Color.black.opacity(0.65))
} }
private var widgetSize: CGSize {
CGSize(width: CGFloat(widgetWidth), height: Self.preferredHeight)
}
private var pauseIcon: String { private var pauseIcon: String {
timerManager.isPaused ? "play.fill" : "pause.fill" timerManager.isPaused ? "play.fill" : "pause.fill"
} }
@ -175,7 +184,7 @@ struct LockScreenTimerWidget: View {
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.vertical, 18) .padding(.vertical, 18)
.frame(width: Self.preferredSize.width, height: Self.preferredSize.height) .frame(width: widgetSize.width, height: widgetSize.height)
.background(widgetBackground) .background(widgetBackground)
.clipShape(RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous))
.overlay { .overlay {

View file

@ -3648,14 +3648,18 @@ private struct LockScreenPositioningControls: View {
@Default(.lockScreenWeatherVerticalOffset) private var weatherOffset @Default(.lockScreenWeatherVerticalOffset) private var weatherOffset
@Default(.lockScreenMusicVerticalOffset) private var musicOffset @Default(.lockScreenMusicVerticalOffset) private var musicOffset
@Default(.lockScreenTimerVerticalOffset) private var timerOffset @Default(.lockScreenTimerVerticalOffset) private var timerOffset
@Default(.lockScreenMusicPanelWidth) private var musicWidth
@Default(.lockScreenTimerWidgetWidth) private var timerWidth
private let offsetRange: ClosedRange<Double> = -160...160 private let offsetRange: ClosedRange<Double> = -160...160
private let musicWidthRange: ClosedRange<Double> = 320...Double(LockScreenMusicPanel.defaultCollapsedWidth)
private let timerWidthRange: ClosedRange<Double> = 320...LockScreenTimerWidget.defaultWidth
var body: some View { var body: some View {
Section { Section {
let weatherBinding = Binding<Double>( let weatherBinding = Binding<Double>(
get: { weatherOffset }, get: { weatherOffset },
set: { newValue in set: { newValue in
let clampedValue = clamp(newValue) let clampedValue = clampOffset(newValue)
if weatherOffset != clampedValue { if weatherOffset != clampedValue {
weatherOffset = clampedValue weatherOffset = clampedValue
} }
@ -3666,7 +3670,7 @@ private struct LockScreenPositioningControls: View {
let timerBinding = Binding<Double>( let timerBinding = Binding<Double>(
get: { timerOffset }, get: { timerOffset },
set: { newValue in set: { newValue in
let clampedValue = clamp(newValue) let clampedValue = clampOffset(newValue)
if timerOffset != clampedValue { if timerOffset != clampedValue {
timerOffset = clampedValue timerOffset = clampedValue
} }
@ -3677,7 +3681,7 @@ private struct LockScreenPositioningControls: View {
let musicBinding = Binding<Double>( let musicBinding = Binding<Double>(
get: { musicOffset }, get: { musicOffset },
set: { newValue in set: { newValue in
let clampedValue = clamp(newValue) let clampedValue = clampOffset(newValue)
if musicOffset != clampedValue { if musicOffset != clampedValue {
musicOffset = clampedValue musicOffset = clampedValue
} }
@ -3685,7 +3689,35 @@ private struct LockScreenPositioningControls: View {
} }
) )
LockScreenPositioningPreview(weatherOffset: weatherBinding, timerOffset: timerBinding, musicOffset: musicBinding) let musicWidthBinding = Binding<Double>(
get: { musicWidth },
set: { newValue in
let clampedValue = clamp(newValue, within: musicWidthRange)
if musicWidth != clampedValue {
musicWidth = clampedValue
propagateMusicWidthChange(animated: false)
}
}
)
let timerWidthBinding = Binding<Double>(
get: { timerWidth },
set: { newValue in
let clampedValue = clamp(newValue, within: timerWidthRange)
if timerWidth != clampedValue {
timerWidth = clampedValue
propagateTimerWidthChange(animated: false)
}
}
)
LockScreenPositioningPreview(
weatherOffset: weatherBinding,
timerOffset: timerBinding,
musicOffset: musicBinding,
musicWidth: musicWidthBinding,
timerWidth: timerWidthBinding
)
.frame(height: 260) .frame(height: 260)
.padding(.vertical, 8) .padding(.vertical, 8)
@ -3719,18 +3751,45 @@ private struct LockScreenPositioningControls: View {
Spacer() Spacer()
} }
Divider()
.padding(.vertical, 8)
VStack(alignment: .leading, spacing: 16) {
widthSlider(
title: "Media Panel Width",
value: musicWidthBinding,
range: musicWidthRange,
resetTitle: "Reset Media Width",
resetAction: resetMusicWidth,
helpText: "Shrinks the lock screen media panel while keeping the expanded view full width."
)
widthSlider(
title: "Timer Widget Width",
value: timerWidthBinding,
range: timerWidthRange,
resetTitle: "Reset Timer Width",
resetAction: resetTimerWidth,
helpText: "Adjusts the lock screen timer widget width without affecting button sizing."
)
}
} header: { } header: {
Text("Lock Screen Positioning") Text("Lock Screen Positioning")
} footer: { } footer: {
Text("Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Changes apply instantly while the widgets are visible.") Text("Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Use the width sliders below to narrow the media and timer widgets without exceeding their default size. Changes apply instantly while the widgets are visible.")
.textCase(nil) .textCase(nil)
} }
} }
private func clamp(_ value: Double) -> Double { private func clampOffset(_ value: Double) -> Double {
min(max(value, offsetRange.lowerBound), offsetRange.upperBound) min(max(value, offsetRange.lowerBound), offsetRange.upperBound)
} }
private func clamp(_ value: Double, within range: ClosedRange<Double>) -> Double {
min(max(value, range.lowerBound), range.upperBound)
}
private func resetWeatherOffset() { private func resetWeatherOffset() {
weatherOffset = 0 weatherOffset = 0
propagateWeatherOffsetChange(animated: true) propagateWeatherOffsetChange(animated: true)
@ -3746,6 +3805,16 @@ private struct LockScreenPositioningControls: View {
propagateMusicOffsetChange(animated: true) propagateMusicOffsetChange(animated: true)
} }
private func resetMusicWidth() {
musicWidth = Double(LockScreenMusicPanel.defaultCollapsedWidth)
propagateMusicWidthChange(animated: true)
}
private func resetTimerWidth() {
timerWidth = LockScreenTimerWidget.defaultWidth
propagateTimerWidthChange(animated: true)
}
private func propagateWeatherOffsetChange(animated: Bool) { private func propagateWeatherOffsetChange(animated: Bool) {
Task { @MainActor in Task { @MainActor in
LockScreenWeatherPanelManager.shared.refreshPositionForOffsets(animated: animated) LockScreenWeatherPanelManager.shared.refreshPositionForOffsets(animated: animated)
@ -3764,6 +3833,18 @@ private struct LockScreenPositioningControls: View {
} }
} }
private func propagateMusicWidthChange(animated: Bool) {
Task { @MainActor in
LockScreenPanelManager.shared.applyOffsetAdjustment(animated: animated)
}
}
private func propagateTimerWidthChange(animated: Bool) {
Task { @MainActor in
LockScreenTimerWidgetPanelManager.shared.refreshPosition(animated: animated)
}
}
@ViewBuilder @ViewBuilder
private func offsetColumn(title: String, value: Double, resetTitle: String, resetAction: @escaping () -> Void) -> some View { private func offsetColumn(title: String, value: Double, resetTitle: String, resetAction: @escaping () -> Void) -> some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
@ -3781,15 +3862,57 @@ private struct LockScreenPositioningControls: View {
} }
} }
@ViewBuilder
private func widthSlider(
title: String,
value: Binding<Double>,
range: ClosedRange<Double>,
resetTitle: String,
resetAction: @escaping () -> Void,
helpText: String
) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(title)
Spacer()
Text(formattedWidth(value.wrappedValue))
.font(.caption)
.foregroundStyle(.secondary)
}
Slider(value: value, in: range)
HStack(alignment: .top) {
Button(resetTitle) {
resetAction()
}
.buttonStyle(.bordered)
Spacer()
Text(helpText)
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
}
}
private func formattedPoints(_ value: Double) -> String { private func formattedPoints(_ value: Double) -> String {
String(format: "%+.0f", value) String(format: "%+.0f", value)
} }
private func formattedWidth(_ value: Double) -> String {
String(format: "%.0f pt", value)
}
} }
private struct LockScreenPositioningPreview: View { private struct LockScreenPositioningPreview: View {
@Binding var weatherOffset: Double @Binding var weatherOffset: Double
@Binding var timerOffset: Double @Binding var timerOffset: Double
@Binding var musicOffset: Double @Binding var musicOffset: Double
@Binding var musicWidth: Double
@Binding var timerWidth: Double
@State private var weatherStartOffset: Double = 0 @State private var weatherStartOffset: Double = 0
@State private var timerStartOffset: Double = 0 @State private var timerStartOffset: Double = 0
@ -3815,8 +3938,17 @@ private struct LockScreenPositioningPreview: View {
let timerBaseY = screenRect.minY + (screenRect.height * 0.5) let timerBaseY = screenRect.minY + (screenRect.height * 0.5)
let musicBaseY = screenRect.minY + (screenRect.height * 0.78) let musicBaseY = screenRect.minY + (screenRect.height * 0.78)
let weatherSize = CGSize(width: screenRect.width * 0.42, height: screenRect.height * 0.22) let weatherSize = CGSize(width: screenRect.width * 0.42, height: screenRect.height * 0.22)
let timerSize = CGSize(width: screenRect.width * 0.5, height: screenRect.height * 0.2) let defaultMusicWidth = Double(LockScreenMusicPanel.defaultCollapsedWidth)
let musicSize = CGSize(width: screenRect.width * 0.56, height: screenRect.height * 0.34) let musicWidthScale = CGFloat(musicWidth / defaultMusicWidth)
let timerWidthScale = CGFloat(timerWidth / LockScreenTimerWidget.defaultWidth)
let timerSize = CGSize(
width: (screenRect.width * 0.5) * timerWidthScale,
height: screenRect.height * 0.2
)
let musicSize = CGSize(
width: (screenRect.width * 0.56) * musicWidthScale,
height: screenRect.height * 0.34
)
ZStack { ZStack {
RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous) RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous)
@ -3844,6 +3976,9 @@ private struct LockScreenPositioningPreview: View {
} }
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset) .animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset)
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset) .animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset)
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: timerOffset)
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicWidth)
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: timerWidth)
} }
private func weatherPanel(size: CGSize) -> some View { private func weatherPanel(size: CGSize) -> some View {

View file

@ -10,6 +10,7 @@ import AppKit
import SkyLightWindow import SkyLightWindow
import Defaults import Defaults
import QuartzCore import QuartzCore
import Combine
@MainActor @MainActor
final class LockScreenPanelAnimator: ObservableObject { final class LockScreenPanelAnimator: ObservableObject {
@ -32,10 +33,12 @@ class LockScreenPanelManager {
private var hideTask: Task<Void, Never>? private var hideTask: Task<Void, Never>?
private var screenChangeObserver: NSObjectProtocol? private var screenChangeObserver: NSObjectProtocol?
private var workspaceObservers: [NSObjectProtocol] = [] private var workspaceObservers: [NSObjectProtocol] = []
private var cancellables = Set<AnyCancellable>()
private init() { private init() {
print("[\(timestamp())] LockScreenPanelManager: initialized") print("[\(timestamp())] LockScreenPanelManager: initialized")
registerScreenChangeObservers() registerScreenChangeObservers()
observeDefaultChanges()
} }
private func timestamp() -> String { private func timestamp() -> String {
@ -120,6 +123,7 @@ class LockScreenPanelManager {
let hosting = NSHostingView(rootView: LockScreenMusicPanel(animator: panelAnimator)) let hosting = NSHostingView(rootView: LockScreenMusicPanel(animator: panelAnimator))
hosting.frame = NSRect(origin: .zero, size: targetFrame.size) hosting.frame = NSRect(origin: .zero, size: targetFrame.size)
hosting.autoresizingMask = [.width, .height]
window.contentView = hosting window.contentView = hosting
// Ensure the underlying window content is clipped to rounded corners // Ensure the underlying window content is clipped to rounded corners
@ -235,6 +239,15 @@ class LockScreenPanelManager {
print("[\(timestamp())] LockScreenPanelManager: realigned window due to \(reason)") print("[\(timestamp())] LockScreenPanelManager: realigned window due to \(reason)")
} }
private func observeDefaultChanges() {
Defaults.publisher(.lockScreenMusicPanelWidth)
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.applyOffsetAdjustment(animated: true)
}
.store(in: &cancellables)
}
private func collapsedFrame(for screenFrame: NSRect) -> NSRect { private func collapsedFrame(for screenFrame: NSRect) -> NSRect {
let collapsedSize = LockScreenMusicPanel.collapsedSize let collapsedSize = LockScreenMusicPanel.collapsedSize
let originX = screenFrame.midX - (collapsedSize.width / 2) let originX = screenFrame.midX - (collapsedSize.width / 2)

View file

@ -75,6 +75,13 @@ final class LockScreenTimerWidgetManager {
self.refreshPositionForOffsets(animated: true) self.refreshPositionForOffsets(animated: true)
} }
.store(in: &cancellables) .store(in: &cancellables)
Defaults.publisher(.lockScreenTimerWidgetWidth, options: [])
.receive(on: RunLoop.main)
.sink { _ in
LockScreenTimerWidgetPanelManager.shared.refreshPosition(animated: true)
}
.store(in: &cancellables)
} }
private func updateVisibility() { private func updateVisibility() {
@ -114,6 +121,7 @@ final class LockScreenTimerWidgetPanelManager {
let window = ensureWindow() let window = ensureWindow()
let frame = targetFrame(on: screen) let frame = targetFrame(on: screen)
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
updateHostingViewSize(for: window, size: frame.size)
latestFrame = frame latestFrame = frame
window.alphaValue = 1 window.alphaValue = 1
window.orderFrontRegardless() window.orderFrontRegardless()
@ -161,6 +169,7 @@ final class LockScreenTimerWidgetPanelManager {
} else { } else {
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
} }
updateHostingViewSize(for: window, size: frame.size)
latestFrame = frame latestFrame = frame
LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: animated) LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: animated)
} }
@ -212,12 +221,17 @@ final class LockScreenTimerWidgetPanelManager {
let view = LockScreenTimerWidget(animator: animator) let view = LockScreenTimerWidget(animator: animator)
let hosting = NSHostingView(rootView: view) let hosting = NSHostingView(rootView: view)
hosting.frame = NSRect(origin: .zero, size: LockScreenTimerWidget.preferredSize) hosting.frame = NSRect(origin: .zero, size: LockScreenTimerWidget.preferredSize)
hosting.autoresizingMask = [.width, .height]
hosting.wantsLayer = true hosting.wantsLayer = true
hosting.layer?.masksToBounds = true hosting.layer?.masksToBounds = true
hosting.layer?.cornerRadius = LockScreenTimerWidget.cornerRadius hosting.layer?.cornerRadius = LockScreenTimerWidget.cornerRadius
return hosting return hosting
} }
private func updateHostingViewSize(for window: NSWindow, size: CGSize) {
window.contentView?.frame = NSRect(origin: .zero, size: NSSize(width: size.width, height: size.height))
}
private func targetFrame(on screen: NSScreen) -> NSRect { private func targetFrame(on screen: NSScreen) -> NSRect {
let size = LockScreenTimerWidget.preferredSize let size = LockScreenTimerWidget.preferredSize
let originX = screen.frame.midX - (size.width / 2) let originX = screen.frame.midX - (size.width / 2)

View file

@ -700,8 +700,10 @@ extension Defaults.Keys {
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo) static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0) static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0)
static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0) static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0)
static let lockScreenMusicPanelWidth = Key<Double>("lockScreenMusicPanelWidth", default: 420)
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false) static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0) static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
static let lockScreenTimerWidgetWidth = Key<Double>("lockScreenTimerWidgetWidth", default: 420)
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid) static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
static let lockScreenGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>( static let lockScreenGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
"lockScreenGlassCustomizationMode", "lockScreenGlassCustomizationMode",