Add Repositionable Widgets modifier

This commit is contained in:
Hariharan Mudaliar 2025-11-20 11:47:54 +05:30
commit bd23b5f210

View file

@ -1813,108 +1813,152 @@ private struct LockScreenPositioningPreview: View {
@State private var isWeatherDragging = false @State private var isWeatherDragging = false
@State private var isMusicDragging = false @State private var isMusicDragging = false
private let offsetRange: ClosedRange<Double> = -160...160
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
let centerX = geometry.size.width / 2 let screenPadding: CGFloat = 26
let weatherBaseY = geometry.size.height * 0.34 let screenCornerRadius: CGFloat = 28
let musicBaseY = geometry.size.height * 0.70 let screenRect = CGRect(
x: screenPadding,
y: screenPadding,
width: geometry.size.width - (screenPadding * 2),
height: geometry.size.height - (screenPadding * 2)
)
let centerX = screenRect.midX
let weatherBaseY = screenRect.minY + (screenRect.height * 0.28)
let musicBaseY = screenRect.minY + (screenRect.height * 0.68)
let weatherSize = CGSize(width: screenRect.width * 0.42, height: screenRect.height * 0.22)
let musicSize = CGSize(width: screenRect.width * 0.56, height: screenRect.height * 0.34)
ZStack { ZStack {
RoundedRectangle(cornerRadius: 22, style: .continuous) RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous)
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.6)) .fill(Color(nsColor: .windowBackgroundColor).opacity(0.55))
RoundedRectangle(cornerRadius: 22, style: .continuous) .frame(width: screenRect.width, height: screenRect.height)
.stroke(Color.white.opacity(0.08), lineWidth: 1) .overlay(
RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous)
.stroke(Color.white.opacity(0.12), lineWidth: 1)
)
.shadow(color: Color.black.opacity(0.22), radius: 20, x: 0, y: 18)
.position(x: screenRect.midX, y: screenRect.midY)
weatherPanel weatherPanel(size: weatherSize)
.position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset)) .position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset))
.gesture(weatherDragGesture) .gesture(weatherDragGesture(in: screenRect, baseY: weatherBaseY, panelSize: weatherSize))
musicPanel musicPanel(size: musicSize)
.position(x: centerX, y: musicBaseY - CGFloat(musicOffset)) .position(x: centerX, y: musicBaseY - CGFloat(musicOffset))
.gesture(musicDragGesture) .gesture(musicDragGesture(in: screenRect, baseY: musicBaseY, panelSize: musicSize))
} }
} }
.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)
} }
private var weatherPanel: some View { private func weatherPanel(size: CGSize) -> some View {
RoundedRectangle(cornerRadius: 18, style: .continuous) RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill( .fill(
LinearGradient( LinearGradient(
colors: [Color.blue.opacity(0.72), Color.blue.opacity(0.48)], colors: [Color.blue.opacity(0.78), Color.blue.opacity(0.52)],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
) )
) )
.frame(width: 220, height: 88) .frame(width: size.width, height: size.height)
.overlay(alignment: .leading) { .overlay(alignment: .leading) {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 4) {
Label("Weather", systemImage: "cloud.sun.fill") Label("Weather", systemImage: "cloud.sun.fill")
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 12, weight: .semibold))
.foregroundStyle(Color.white) .foregroundStyle(Color.white)
Text("Inline snapshot preview") Text("Inline snapshot preview")
.font(.system(size: 11, weight: .medium)) .font(.system(size: 10, weight: .medium))
.foregroundStyle(Color.white.opacity(0.7)) .foregroundStyle(Color.white.opacity(0.72))
}
.padding(.horizontal, 16)
}
.shadow(color: Color.blue.opacity(0.22), radius: 10, x: 0, y: 8)
}
private func musicPanel(size: CGSize) -> some View {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(
LinearGradient(
colors: [Color.purple.opacity(0.68), Color.pink.opacity(0.5)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: size.width, height: size.height)
.overlay(alignment: .leading) {
VStack(alignment: .leading, spacing: 6) {
Label("Media", systemImage: "play.square.stack")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Color.white)
Text("Lock screen panel preview")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(Color.white.opacity(0.72))
} }
.padding(.horizontal, 18) .padding(.horizontal, 18)
} }
.shadow(color: Color.blue.opacity(0.25), radius: 12, x: 0, y: 8) .shadow(color: Color.purple.opacity(0.24), radius: 12, x: 0, y: 9)
} }
private var musicPanel: some View { private func weatherDragGesture(in screenRect: CGRect, baseY: CGFloat, panelSize: CGSize) -> some Gesture {
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) DragGesture(minimumDistance: 0)
.onChanged { value in .onChanged { value in
if !isWeatherDragging { if !isWeatherDragging {
isWeatherDragging = true isWeatherDragging = true
weatherStartOffset = weatherOffset weatherStartOffset = weatherOffset
} }
let proposed = weatherStartOffset - Double(value.translation.height) let proposed = weatherStartOffset - Double(value.translation.height)
weatherOffset = proposed weatherOffset = clampedOffset(
proposed,
baseCenterY: baseY,
panelHeight: panelSize.height,
screenRect: screenRect
)
} }
.onEnded { _ in .onEnded { _ in
isWeatherDragging = false isWeatherDragging = false
} }
} }
private var musicDragGesture: some Gesture { private func musicDragGesture(in screenRect: CGRect, baseY: CGFloat, panelSize: CGSize) -> some Gesture {
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { value in .onChanged { value in
if !isMusicDragging { if !isMusicDragging {
isMusicDragging = true isMusicDragging = true
musicStartOffset = musicOffset musicStartOffset = musicOffset
} }
let proposed = musicStartOffset - Double(value.translation.height) let proposed = musicStartOffset - Double(value.translation.height)
musicOffset = proposed musicOffset = clampedOffset(
proposed,
baseCenterY: baseY,
panelHeight: panelSize.height,
screenRect: screenRect
)
} }
.onEnded { _ in .onEnded { _ in
isMusicDragging = false isMusicDragging = false
} }
} }
private func clampedOffset(
_ proposed: Double,
baseCenterY: CGFloat,
panelHeight: CGFloat,
screenRect: CGRect
) -> Double {
let halfHeight = panelHeight / 2
let minCenterY = screenRect.minY + halfHeight
let maxCenterY = screenRect.maxY - halfHeight
let proposedCenter = baseCenterY - CGFloat(proposed)
let clampedCenter = min(max(proposedCenter, minCenterY), maxCenterY)
let derivedOffset = Double(baseCenterY - clampedCenter)
return min(max(derivedOffset, offsetRange.lowerBound), offsetRange.upperBound)
}
} }
struct Shortcuts: View { struct Shortcuts: View {