Reminder Widget dynamic positioning

This commit is contained in:
Hariharan Mudaliar 2025-12-03 23:31:37 +05:30
commit 1544a666c3
2 changed files with 46 additions and 13 deletions

View file

@ -1,6 +1,7 @@
import AppKit import AppKit
import SwiftUI import SwiftUI
import SkyLightWindow import SkyLightWindow
import QuartzCore
@MainActor @MainActor
final class LockScreenReminderWidgetPanelManager { final class LockScreenReminderWidgetPanelManager {
@ -29,6 +30,20 @@ final class LockScreenReminderWidgetPanelManager {
window.contentView = nil window.contentView = nil
} }
func refreshPosition(animated: Bool) {
guard let window, window.isVisible, let screen = NSScreen.main else { return }
let target = frame(for: window.frame.size, on: screen)
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.22
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrame(target, display: true)
}
} else {
window.setFrame(target, display: true)
}
}
private func render(snapshot: LockScreenReminderWidgetSnapshot, makeVisible: Bool) { private func render(snapshot: LockScreenReminderWidgetSnapshot, makeVisible: Bool) {
guard let screen = NSScreen.main else { return } guard let screen = NSScreen.main else { return }
if !makeVisible, window == nil { if !makeVisible, window == nil {
@ -84,26 +99,35 @@ final class LockScreenReminderWidgetPanelManager {
let screenFrame = screen.frame let screenFrame = screen.frame
let originX = screenFrame.midX - (size.width / 2) let originX = screenFrame.midX - (size.width / 2)
let musicTop = LockScreenPanelManager.shared.latestFrame?.maxY ?? (screenFrame.midY - 32) let musicTop = LockScreenPanelManager.shared.latestFrame?.maxY ?? (screenFrame.midY - 32)
let timerFrame = LockScreenTimerWidgetPanelManager.shared.latestFrame
let defaultWeatherBottom = screenFrame.midY + (screenFrame.height * 0.14)
let weatherBottom = LockScreenWeatherPanelManager.shared.latestFrame?.minY ?? min(screenFrame.maxY - 120, max(defaultWeatherBottom, musicTop + size.height + 80))
let marginAboveMusic: CGFloat = 16 let marginAboveMusic: CGFloat = 16
let marginAboveTimer: CGFloat = 24
let marginBelowWeather: CGFloat = 16 let marginBelowWeather: CGFloat = 16
let constrainedWeatherBottom = max(weatherBottom, musicTop + size.height + marginAboveMusic + marginBelowWeather) let baseLowerBound = musicTop + marginAboveMusic
let centerY = (musicTop + constrainedWeatherBottom) / 2 let timerLowerBound = timerFrame.map { $0.maxY + marginAboveTimer }
let lowerBound = screenFrame.minY + 80 let minYFloor = screenFrame.minY + 80
let upperBound = constrainedWeatherBottom - marginBelowWeather - size.height let lowerBound = max(timerLowerBound ?? baseLowerBound, minYFloor)
var proposedY = centerY - (size.height / 2) let defaultWeatherBottom = screenFrame.midY + (screenFrame.height * 0.14)
if proposedY < lowerBound { let minimumWeatherBottom = lowerBound + size.height + marginBelowWeather
proposedY = lowerBound let weatherBottom = LockScreenWeatherPanelManager.shared.latestFrame?.minY ?? min(screenFrame.maxY - 120, max(defaultWeatherBottom, minimumWeatherBottom))
}
if upperBound > lowerBound { let upperBound = weatherBottom - marginBelowWeather - size.height
proposedY = min(proposedY, upperBound) let clampedUpperBound = max(upperBound, lowerBound)
let prefersTopAlignment = timerFrame != nil
var proposedY: CGFloat
if prefersTopAlignment {
proposedY = clampedUpperBound
} else {
proposedY = (lowerBound + clampedUpperBound) / 2
} }
proposedY = max(lowerBound, min(proposedY, clampedUpperBound))
return NSRect(x: originX, y: proposedY, width: size.width, height: size.height) return NSRect(x: originX, y: proposedY, width: size.width, height: size.height)
} }
} }

View file

@ -101,6 +101,7 @@ final class LockScreenTimerWidgetPanelManager {
private var hasDelegated = false private var hasDelegated = false
private let animator = LockScreenTimerWidgetAnimator() private let animator = LockScreenTimerWidgetAnimator()
private var hideTask: Task<Void, Never>? private var hideTask: Task<Void, Never>?
private(set) var latestFrame: NSRect?
private init() {} private init() {}
@ -109,11 +110,13 @@ 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)
latestFrame = frame
window.alphaValue = 1 window.alphaValue = 1
window.orderFrontRegardless() window.orderFrontRegardless()
hideTask?.cancel() hideTask?.cancel()
hideTask = nil hideTask = nil
animator.isPresented = true animator.isPresented = true
LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true)
} }
func hide(animated: Bool = true) { func hide(animated: Bool = true) {
@ -125,6 +128,8 @@ final class LockScreenTimerWidgetPanelManager {
if delay == 0 { if delay == 0 {
window.orderOut(nil) window.orderOut(nil)
hideTask = nil hideTask = nil
latestFrame = nil
LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true)
return return
} }
@ -133,6 +138,8 @@ final class LockScreenTimerWidgetPanelManager {
await MainActor.run { await MainActor.run {
window?.orderOut(nil) window?.orderOut(nil)
self?.hideTask = nil self?.hideTask = nil
self?.latestFrame = nil
LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true)
} }
} }
} }
@ -150,6 +157,8 @@ final class LockScreenTimerWidgetPanelManager {
} else { } else {
window.setFrame(frame, display: true) window.setFrame(frame, display: true)
} }
latestFrame = frame
LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: animated)
} }
func refreshRelativeToMusicPanel(animated: Bool) { func refreshRelativeToMusicPanel(animated: Bool) {