Added positioning mocks

This commit is contained in:
Hariharan Mudaliar 2025-11-20 11:25:33 +05:30
commit 0a6b4a576b
5 changed files with 310 additions and 7 deletions

View file

@ -751,6 +751,14 @@
}
}
},
"%@ Offset" : {
"comment" : "A control within the lock screen settings that allows the user to adjust the offset for a specific dimension (e.g. width or height). The argument is the name of the dimension, and the second argument is",
"isCommentAutoGenerated" : true
},
"%@ pt" : {
"comment" : "A text view displaying the current offset value for a specific lock screen control. The argument is the name of the control.",
"isCommentAutoGenerated" : true
},
"%@°" : {
"comment" : "A label inside the temperature gauge that shows the current temperature. The value is shown in degrees Celsius (or Fahrenheit, if specified in the snapshot).",
"isCommentAutoGenerated" : true
@ -7052,6 +7060,9 @@
}
}
}
},
"Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Changes apply instantly while the widgets are visible." : {
},
"Drop files here" : {
"localizations" : {
@ -10225,6 +10236,9 @@
}
}
}
},
"Inline snapshot preview" : {
},
"Install updates automatically" : {
"extractionState" : "stale",
@ -10587,6 +10601,14 @@
},
"Lock screen material" : {
},
"Lock screen panel preview" : {
"comment" : "A description of the content displayed in the lock screen panel.",
"isCommentAutoGenerated" : true
},
"Lock Screen Positioning" : {
"comment" : "The header text for the \"Lock Screen Positioning\" section in the settings view.",
"isCommentAutoGenerated" : true
},
"Lock Screen Reminder Widget" : {
"comment" : "A section header for the lock screen reminder widget settings.",
@ -19766,6 +19788,10 @@
}
}
},
"Weather" : {
"comment" : "A label and its accompanying text displayed in a lock screen panel.",
"isCommentAutoGenerated" : true
},
"Weather data provider" : {
},

View file

@ -1436,6 +1436,8 @@ struct Appearance: View {
Text("General")
}
LockScreenPositioningControls()
Section {
Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
Defaults
@ -1693,6 +1695,228 @@ struct Appearance: View {
}
}
private struct LockScreenPositioningControls: View {
@Default(.lockScreenWeatherVerticalOffset) private var weatherOffset
@Default(.lockScreenMusicVerticalOffset) private var musicOffset
private let offsetRange: ClosedRange<Double> = -160...160
var body: some View {
Section {
let weatherBinding = Binding<Double>(
get: { weatherOffset },
set: { newValue in
let clampedValue = clamp(newValue)
if weatherOffset != clampedValue {
weatherOffset = clampedValue
}
propagateWeatherOffsetChange(animated: false)
}
)
let musicBinding = Binding<Double>(
get: { musicOffset },
set: { newValue in
let clampedValue = clamp(newValue)
if musicOffset != clampedValue {
musicOffset = clampedValue
}
propagateMusicOffsetChange(animated: false)
}
)
LockScreenPositioningPreview(weatherOffset: weatherBinding, musicOffset: musicBinding)
.frame(height: 260)
.padding(.vertical, 8)
HStack(alignment: .top, spacing: 24) {
offsetColumn(
title: "Weather",
value: weatherOffset,
resetTitle: "Reset Weather",
resetAction: resetWeatherOffset
)
Divider()
.frame(height: 64)
offsetColumn(
title: "Music",
value: musicOffset,
resetTitle: "Reset Music",
resetAction: resetMusicOffset
)
Spacer()
}
} header: {
Text("Lock Screen Positioning")
} 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.")
.textCase(nil)
}
}
private func clamp(_ value: Double) -> Double {
min(max(value, offsetRange.lowerBound), offsetRange.upperBound)
}
private func resetWeatherOffset() {
weatherOffset = 0
propagateWeatherOffsetChange(animated: true)
}
private func resetMusicOffset() {
musicOffset = 0
propagateMusicOffsetChange(animated: true)
}
private func propagateWeatherOffsetChange(animated: Bool) {
Task { @MainActor in
LockScreenWeatherPanelManager.shared.refreshPositionForOffsets(animated: animated)
}
}
private func propagateMusicOffsetChange(animated: Bool) {
Task { @MainActor in
LockScreenPanelManager.shared.applyOffsetAdjustment(animated: animated)
}
}
@ViewBuilder
private func offsetColumn(title: String, value: Double, resetTitle: String, resetAction: @escaping () -> Void) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text("\(title) Offset")
.font(.subheadline.weight(.semibold))
Text("\(formattedPoints(value)) pt")
.font(.caption)
.foregroundStyle(.secondary)
Button(resetTitle) {
resetAction()
}
.buttonStyle(.bordered)
}
}
private func formattedPoints(_ value: Double) -> String {
String(format: "%+.0f", value)
}
}
private struct LockScreenPositioningPreview: View {
@Binding var weatherOffset: Double
@Binding var musicOffset: Double
@State private var weatherStartOffset: Double = 0
@State private var musicStartOffset: Double = 0
@State private var isWeatherDragging = false
@State private var isMusicDragging = false
var body: some View {
GeometryReader { geometry in
let centerX = geometry.size.width / 2
let weatherBaseY = geometry.size.height * 0.34
let musicBaseY = geometry.size.height * 0.70
ZStack {
RoundedRectangle(cornerRadius: 22, style: .continuous)
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.6))
RoundedRectangle(cornerRadius: 22, style: .continuous)
.stroke(Color.white.opacity(0.08), lineWidth: 1)
weatherPanel
.position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset))
.gesture(weatherDragGesture)
musicPanel
.position(x: centerX, y: musicBaseY - CGFloat(musicOffset))
.gesture(musicDragGesture)
}
}
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset)
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset)
}
private var weatherPanel: some View {
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(
LinearGradient(
colors: [Color.blue.opacity(0.72), Color.blue.opacity(0.48)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 220, height: 88)
.overlay(alignment: .leading) {
VStack(alignment: .leading, spacing: 6) {
Label("Weather", systemImage: "cloud.sun.fill")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Color.white)
Text("Inline snapshot preview")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Color.white.opacity(0.7))
}
.padding(.horizontal, 18)
}
.shadow(color: Color.blue.opacity(0.25), radius: 12, x: 0, y: 8)
}
private var musicPanel: some View {
RoundedRectangle(cornerRadius: 22, style: .continuous)
.fill(
LinearGradient(
colors: [Color.purple.opacity(0.68), Color.pink.opacity(0.52)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 280, height: 132)
.overlay(alignment: .leading) {
VStack(alignment: .leading, spacing: 8) {
Label("Media", systemImage: "play.square.stack")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color.white)
Text("Lock screen panel preview")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Color.white.opacity(0.7))
}
.padding(.horizontal, 22)
}
.shadow(color: Color.purple.opacity(0.3), radius: 16, x: 0, y: 10)
}
private var weatherDragGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
if !isWeatherDragging {
isWeatherDragging = true
weatherStartOffset = weatherOffset
}
let proposed = weatherStartOffset - Double(value.translation.height)
weatherOffset = proposed
}
.onEnded { _ in
isWeatherDragging = false
}
}
private var musicDragGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
if !isMusicDragging {
isMusicDragging = true
musicStartOffset = musicOffset
}
let proposed = musicStartOffset - Double(value.translation.height)
musicOffset = proposed
}
.onEnded { _ in
isMusicDragging = false
}
}
}
struct Shortcuts: View {
@Default(.enableTimerFeature) var enableTimerFeature
@Default(.enableClipboardManager) var enableClipboardManager

View file

@ -23,6 +23,8 @@ class LockScreenPanelManager {
private var panelWindow: NSWindow?
private var hasDelegated = false
private var collapsedFrame: NSRect?
private var isPanelExpanded = false
private var currentAdditionalHeight: CGFloat = 0
private let collapsedPanelCornerRadius: CGFloat = 28
private let expandedPanelCornerRadius: CGFloat = 52
private(set) var latestFrame: NSRect?
@ -53,13 +55,11 @@ class LockScreenPanelManager {
return
}
let collapsedSize = LockScreenMusicPanel.collapsedSize
let screenFrame = screen.frame
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
let originX = centerX - (collapsedSize.width / 2)
let originY = screenFrame.origin.y + (screenFrame.height / 2) - collapsedSize.height - 32
let targetFrame = NSRect(x: originX, y: originY, width: collapsedSize.width, height: collapsedSize.height)
let targetFrame = collapsedFrame(for: screenFrame)
collapsedFrame = targetFrame
isPanelExpanded = false
currentAdditionalHeight = 0
let window: NSWindow
@ -152,6 +152,19 @@ class LockScreenPanelManager {
} else {
window.contentView?.layer?.cornerRadius = targetRadius
}
isPanelExpanded = expanded
currentAdditionalHeight = additionalHeight
}
func applyOffsetAdjustment(animated: Bool = true) {
guard let screen = NSScreen.main else { return }
let screenFrame = screen.frame
let newCollapsed = collapsedFrame(for: screenFrame)
collapsedFrame = newCollapsed
guard panelWindow != nil else { return }
updatePanelSize(expanded: isPanelExpanded, additionalHeight: currentAdditionalHeight, animated: animated)
}
func hidePanel() {
@ -177,4 +190,14 @@ class LockScreenPanelManager {
}
}
}
private func collapsedFrame(for screenFrame: NSRect) -> NSRect {
let collapsedSize = LockScreenMusicPanel.collapsedSize
let originX = screenFrame.midX - (collapsedSize.width / 2)
let baseOriginY = screenFrame.origin.y + (screenFrame.height / 2) - collapsedSize.height - 32
let userOffset = CGFloat(Defaults[.lockScreenMusicVerticalOffset])
let clampedOffset = min(max(userOffset, -160), 160)
let originY = baseOriginY + clampedOffset
return NSRect(x: originX, y: originY, width: collapsedSize.width, height: collapsedSize.height)
}
}

View file

@ -1,6 +1,8 @@
import AppKit
import SwiftUI
import SkyLightWindow
import Defaults
import QuartzCore
@MainActor
final class LockScreenWeatherPanelManager {
@ -9,6 +11,8 @@ final class LockScreenWeatherPanelManager {
private var window: NSWindow?
private var hasDelegated = false
private(set) var latestFrame: NSRect?
private var lastSnapshot: LockScreenWeatherSnapshot?
private var lastContentSize: CGSize?
private init() {}
@ -25,6 +29,8 @@ final class LockScreenWeatherPanelManager {
window.orderOut(nil)
window.contentView = nil
latestFrame = nil
lastSnapshot = nil
lastContentSize = nil
}
private func render(snapshot: LockScreenWeatherSnapshot, makeVisible: Bool) {
@ -43,6 +49,8 @@ final class LockScreenWeatherPanelManager {
window.setFrame(targetFrame, display: true)
latestFrame = targetFrame
window.contentView = hostingView
lastSnapshot = snapshot
lastContentSize = fittingSize
if makeVisible {
window.orderFrontRegardless()
@ -86,10 +94,30 @@ final class LockScreenWeatherPanelManager {
let baseY = min(maxY, screenFrame.midY + verticalOffset)
let loweredY = baseY - 36
let inlineLift: CGFloat = snapshot.widgetStyle == .inline ? 28 : 0
let adjustedY = loweredY + inlineLift
let inlineLift: CGFloat = snapshot.widgetStyle == .inline ? 44 : 0
let userOffset = CGFloat(Defaults[.lockScreenWeatherVerticalOffset])
let clampedOffset = min(max(userOffset, -160), 160)
let adjustedY = loweredY + inlineLift + clampedOffset
let upperClampedY = min(maxY, adjustedY)
let clampedY = max(screenFrame.minY + 80, upperClampedY)
return NSRect(x: originX, y: clampedY, width: size.width, height: size.height)
}
func refreshPositionForOffsets(animated: Bool = true) {
guard let window, let snapshot = lastSnapshot else { return }
guard let screen = NSScreen.main else { return }
let size = lastContentSize ?? window.frame.size
let targetFrame = frame(for: size, snapshot: snapshot, on: screen)
latestFrame = targetFrame
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.22
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrame(targetFrame, display: true)
}
} else {
window.setFrame(targetFrame, display: true)
}
}
}

View file

@ -438,6 +438,8 @@ extension Defaults.Keys {
static let lockScreenWeatherAQIScale = Key<LockScreenWeatherAirQualityScale>("lockScreenWeatherAQIScale", default: .us)
static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: false)
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0)
static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0)
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true)