Improve animations and resizing again. Add option for animation speed.
This commit is contained in:
@@ -151,7 +151,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var sizeAnimation: Animation? {
|
||||
guard !vm.isUserResizing else { return nil }
|
||||
guard !vm.isUserResizing, !vm.isPresetResizing else { return nil }
|
||||
return vm.notchState == .open ? vm.openAnimation : vm.closeAnimation
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ class ScreenManager: ObservableObject {
|
||||
|
||||
static let shared = ScreenManager()
|
||||
private let focusRetryDelay: TimeInterval = 0.01
|
||||
private let presetResizeFrameInterval: TimeInterval = 1.0 / 60.0
|
||||
|
||||
private(set) var windows: [String: NotchWindow] = [:]
|
||||
private(set) var viewModels: [String: NotchViewModel] = [:]
|
||||
private var presetResizeTimers: [String: Timer] = [:]
|
||||
|
||||
@AppStorage(NotchSettings.Keys.showOnAllDisplays)
|
||||
private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
|
||||
@@ -140,10 +142,9 @@ class ScreenManager: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
withAnimation(vm.openAnimation) {
|
||||
vm.applySizePreset(preset, notifyWindowResize: false)
|
||||
}
|
||||
updateWindowFrame(for: screenUUID, centerHorizontally: true)
|
||||
let startSize = vm.notchSize
|
||||
let targetSize = vm.setStoredOpenSize(preset.size)
|
||||
animatePresetResize(for: screenUUID, from: startSize, to: targetSize, duration: vm.openAnimationDuration)
|
||||
}
|
||||
|
||||
// MARK: - Window creation
|
||||
@@ -236,10 +237,28 @@ class ScreenManager: ObservableObject {
|
||||
window: NotchWindow,
|
||||
centerHorizontally: Bool = false
|
||||
) {
|
||||
guard let vm = viewModels[screenUUID] else { return }
|
||||
let frame = targetWindowFrame(
|
||||
for: screenUUID,
|
||||
on: screen,
|
||||
window: window,
|
||||
centerHorizontally: centerHorizontally,
|
||||
contentSize: nil
|
||||
)
|
||||
guard !window.frame.equalTo(frame) else { return }
|
||||
window.setFrame(frame, display: false)
|
||||
}
|
||||
|
||||
private func targetWindowFrame(
|
||||
for screenUUID: String,
|
||||
on screen: NSScreen,
|
||||
window: NotchWindow,
|
||||
centerHorizontally: Bool,
|
||||
contentSize: CGSize?
|
||||
) -> NSRect {
|
||||
guard let vm = viewModels[screenUUID] else { return window.frame }
|
||||
|
||||
let shadowPadding: CGFloat = 20
|
||||
let openSize = vm.openNotchSize
|
||||
let openSize = contentSize ?? vm.openNotchSize
|
||||
let windowWidth = openSize.width + 40
|
||||
let windowHeight = openSize.height + shadowPadding
|
||||
let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2
|
||||
@@ -248,12 +267,87 @@ class ScreenManager: ObservableObject {
|
||||
? centeredX
|
||||
: min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth)
|
||||
|
||||
let frame = NSRect(
|
||||
return NSRect(
|
||||
x: x,
|
||||
y: screen.frame.origin.y + screen.frame.height - windowHeight,
|
||||
width: windowWidth,
|
||||
height: windowHeight
|
||||
)
|
||||
}
|
||||
|
||||
private func animatePresetResize(
|
||||
for screenUUID: String,
|
||||
from startSize: CGSize,
|
||||
to targetSize: CGSize,
|
||||
duration: TimeInterval
|
||||
) {
|
||||
cancelPresetResize(for: screenUUID)
|
||||
|
||||
guard let vm = viewModels[screenUUID] else { return }
|
||||
guard startSize != targetSize else {
|
||||
vm.notchSize = targetSize
|
||||
updateWindowFrame(for: screenUUID, centerHorizontally: true)
|
||||
return
|
||||
}
|
||||
|
||||
vm.isPresetResizing = true
|
||||
let startTime = CACurrentMediaTime()
|
||||
let duration = max(duration, presetResizeFrameInterval)
|
||||
|
||||
let timer = Timer(timeInterval: presetResizeFrameInterval, repeats: true) { [weak self] timer in
|
||||
MainActor.assumeIsolated {
|
||||
guard let self, let vm = self.viewModels[screenUUID] else {
|
||||
timer.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
let elapsed = CACurrentMediaTime() - startTime
|
||||
let progress = min(1, elapsed / duration)
|
||||
let easedProgress = 0.5 - (cos(.pi * progress) / 2)
|
||||
let size = CGSize(
|
||||
width: startSize.width + ((targetSize.width - startSize.width) * easedProgress),
|
||||
height: startSize.height + ((targetSize.height - startSize.height) * easedProgress)
|
||||
)
|
||||
|
||||
vm.notchSize = size
|
||||
self.updateWindowFrame(for: screenUUID, contentSize: size, centerHorizontally: true)
|
||||
|
||||
if progress >= 1 {
|
||||
vm.notchSize = targetSize
|
||||
vm.isPresetResizing = false
|
||||
self.updateWindowFrame(for: screenUUID, contentSize: targetSize, centerHorizontally: true)
|
||||
self.presetResizeTimers[screenUUID] = nil
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presetResizeTimers[screenUUID] = timer
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
timer.fire()
|
||||
}
|
||||
|
||||
private func cancelPresetResize(for screenUUID: String) {
|
||||
presetResizeTimers[screenUUID]?.invalidate()
|
||||
presetResizeTimers[screenUUID] = nil
|
||||
viewModels[screenUUID]?.isPresetResizing = false
|
||||
}
|
||||
|
||||
private func updateWindowFrame(
|
||||
for screenUUID: String,
|
||||
contentSize: CGSize,
|
||||
centerHorizontally: Bool = false
|
||||
) {
|
||||
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
|
||||
let window = windows[screenUUID] else { return }
|
||||
|
||||
let frame = targetWindowFrame(
|
||||
for: screenUUID,
|
||||
on: screen,
|
||||
window: window,
|
||||
centerHorizontally: centerHorizontally,
|
||||
contentSize: contentSize
|
||||
)
|
||||
guard !window.frame.equalTo(frame) else { return }
|
||||
window.setFrame(frame, display: false)
|
||||
}
|
||||
@@ -261,6 +355,10 @@ class ScreenManager: ObservableObject {
|
||||
// MARK: - Cleanup
|
||||
|
||||
private func cleanupAllWindows() {
|
||||
for (_, timer) in presetResizeTimers {
|
||||
timer.invalidate()
|
||||
}
|
||||
presetResizeTimers.removeAll()
|
||||
for (_, window) in windows {
|
||||
window.orderOut(nil)
|
||||
window.close()
|
||||
|
||||
@@ -37,6 +37,7 @@ enum NotchSettings {
|
||||
static let closeSpringDamping = "closeSpringDamping"
|
||||
static let hoverSpringResponse = "hoverSpringResponse"
|
||||
static let hoverSpringDamping = "hoverSpringDamping"
|
||||
static let resizeAnimationDuration = "resizeAnimationDuration"
|
||||
|
||||
// Behavior
|
||||
static let enableGestures = "enableGestures"
|
||||
@@ -85,6 +86,7 @@ enum NotchSettings {
|
||||
static let closeSpringDamping: Double = 1.0
|
||||
static let hoverSpringResponse: Double = 0.38
|
||||
static let hoverSpringDamping: Double = 0.8
|
||||
static let resizeAnimationDuration: Double = 0.42
|
||||
|
||||
static let enableGestures: Bool = true
|
||||
static let gestureSensitivity: Double = 0.5
|
||||
@@ -132,6 +134,7 @@ enum NotchSettings {
|
||||
Keys.closeSpringDamping: Defaults.closeSpringDamping,
|
||||
Keys.hoverSpringResponse: Defaults.hoverSpringResponse,
|
||||
Keys.hoverSpringDamping: Defaults.hoverSpringDamping,
|
||||
Keys.resizeAnimationDuration: Defaults.resizeAnimationDuration,
|
||||
|
||||
Keys.enableGestures: Defaults.enableGestures,
|
||||
Keys.gestureSensitivity: Defaults.gestureSensitivity,
|
||||
|
||||
@@ -18,6 +18,7 @@ class NotchViewModel: ObservableObject {
|
||||
@Published var isCloseTransitionActive: Bool = false
|
||||
@Published var suppressHoverOpenUntilHoverExit: Bool = false
|
||||
@Published var isUserResizing: Bool = false
|
||||
@Published var isPresetResizing: Bool = false
|
||||
|
||||
let terminalManager = TerminalManager.shared
|
||||
|
||||
@@ -36,6 +37,7 @@ class NotchViewModel: ObservableObject {
|
||||
@AppStorage(NotchSettings.Keys.openSpringDamping) private var openSpringDamping = NotchSettings.Defaults.openSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.resizeAnimationDuration) private var resizeAnimationDurationSetting = NotchSettings.Defaults.resizeAnimationDuration
|
||||
|
||||
private var closeTransitionTask: Task<Void, Never>?
|
||||
|
||||
@@ -45,6 +47,9 @@ class NotchViewModel: ObservableObject {
|
||||
var closeAnimation: Animation {
|
||||
.spring(response: closeSpringResponse, dampingFraction: closeSpringDamping)
|
||||
}
|
||||
var openAnimationDuration: TimeInterval {
|
||||
max(0.05, resizeAnimationDurationSetting)
|
||||
}
|
||||
|
||||
init(screenUUID: String) {
|
||||
self.screenUUID = screenUUID
|
||||
@@ -94,10 +99,16 @@ class NotchViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
|
||||
func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
|
||||
let clampedSize = clampedOpenSize(proposedSize)
|
||||
openWidth = clampedSize.width
|
||||
openHeight = clampedSize.height
|
||||
return clampedSize
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
|
||||
let clampedSize = setStoredOpenSize(proposedSize)
|
||||
if notchState == .open {
|
||||
notchSize = clampedSize
|
||||
}
|
||||
|
||||
@@ -227,6 +227,7 @@ struct AnimationSettingsView: View {
|
||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.resizeAnimationDuration) private var resizeDuration = NotchSettings.Defaults.resizeAnimationDuration
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -239,6 +240,9 @@ struct AnimationSettingsView: View {
|
||||
Section("Hover Animation") {
|
||||
springControls(response: $hoverResponse, damping: $hoverDamping)
|
||||
}
|
||||
Section("Resize Animation") {
|
||||
durationControl(duration: $resizeDuration)
|
||||
}
|
||||
Section {
|
||||
Button("Reset to Defaults") {
|
||||
openResponse = NotchSettings.Defaults.openSpringResponse
|
||||
@@ -247,6 +251,7 @@ struct AnimationSettingsView: View {
|
||||
closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||
hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
resizeDuration = NotchSettings.Defaults.resizeAnimationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +271,15 @@ struct AnimationSettingsView: View {
|
||||
Text(String(format: "%.2f", damping.wrappedValue)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func durationControl(duration: Binding<Double>) -> some View {
|
||||
HStack {
|
||||
Text("Duration")
|
||||
Slider(value: duration, in: 0.05...1.5, step: 0.01)
|
||||
Text(String(format: "%.2fs", duration.wrappedValue)).monospacedDigit().frame(width: 56)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal
|
||||
|
||||
Reference in New Issue
Block a user