Improve animations and resizing again. Add option for animation speed.

This commit is contained in:
2026-03-13 00:14:00 +11:00
parent 256998eb9f
commit e4719cb9f4
6 changed files with 135 additions and 9 deletions

View File

@@ -151,7 +151,7 @@ struct ContentView: View {
} }
private var sizeAnimation: Animation? { 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 return vm.notchState == .open ? vm.openAnimation : vm.closeAnimation
} }

View File

@@ -11,9 +11,11 @@ class ScreenManager: ObservableObject {
static let shared = ScreenManager() static let shared = ScreenManager()
private let focusRetryDelay: TimeInterval = 0.01 private let focusRetryDelay: TimeInterval = 0.01
private let presetResizeFrameInterval: TimeInterval = 1.0 / 60.0
private(set) var windows: [String: NotchWindow] = [:] private(set) var windows: [String: NotchWindow] = [:]
private(set) var viewModels: [String: NotchViewModel] = [:] private(set) var viewModels: [String: NotchViewModel] = [:]
private var presetResizeTimers: [String: Timer] = [:]
@AppStorage(NotchSettings.Keys.showOnAllDisplays) @AppStorage(NotchSettings.Keys.showOnAllDisplays)
private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
@@ -140,10 +142,9 @@ class ScreenManager: ObservableObject {
return return
} }
withAnimation(vm.openAnimation) { let startSize = vm.notchSize
vm.applySizePreset(preset, notifyWindowResize: false) let targetSize = vm.setStoredOpenSize(preset.size)
} animatePresetResize(for: screenUUID, from: startSize, to: targetSize, duration: vm.openAnimationDuration)
updateWindowFrame(for: screenUUID, centerHorizontally: true)
} }
// MARK: - Window creation // MARK: - Window creation
@@ -236,10 +237,28 @@ class ScreenManager: ObservableObject {
window: NotchWindow, window: NotchWindow,
centerHorizontally: Bool = false 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 shadowPadding: CGFloat = 20
let openSize = vm.openNotchSize let openSize = contentSize ?? vm.openNotchSize
let windowWidth = openSize.width + 40 let windowWidth = openSize.width + 40
let windowHeight = openSize.height + shadowPadding let windowHeight = openSize.height + shadowPadding
let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2 let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2
@@ -248,12 +267,87 @@ class ScreenManager: ObservableObject {
? centeredX ? centeredX
: min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth) : min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth)
let frame = NSRect( return NSRect(
x: x, x: x,
y: screen.frame.origin.y + screen.frame.height - windowHeight, y: screen.frame.origin.y + screen.frame.height - windowHeight,
width: windowWidth, width: windowWidth,
height: windowHeight 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 } guard !window.frame.equalTo(frame) else { return }
window.setFrame(frame, display: false) window.setFrame(frame, display: false)
} }
@@ -261,6 +355,10 @@ class ScreenManager: ObservableObject {
// MARK: - Cleanup // MARK: - Cleanup
private func cleanupAllWindows() { private func cleanupAllWindows() {
for (_, timer) in presetResizeTimers {
timer.invalidate()
}
presetResizeTimers.removeAll()
for (_, window) in windows { for (_, window) in windows {
window.orderOut(nil) window.orderOut(nil)
window.close() window.close()

View File

@@ -37,6 +37,7 @@ enum NotchSettings {
static let closeSpringDamping = "closeSpringDamping" static let closeSpringDamping = "closeSpringDamping"
static let hoverSpringResponse = "hoverSpringResponse" static let hoverSpringResponse = "hoverSpringResponse"
static let hoverSpringDamping = "hoverSpringDamping" static let hoverSpringDamping = "hoverSpringDamping"
static let resizeAnimationDuration = "resizeAnimationDuration"
// Behavior // Behavior
static let enableGestures = "enableGestures" static let enableGestures = "enableGestures"
@@ -85,6 +86,7 @@ enum NotchSettings {
static let closeSpringDamping: Double = 1.0 static let closeSpringDamping: Double = 1.0
static let hoverSpringResponse: Double = 0.38 static let hoverSpringResponse: Double = 0.38
static let hoverSpringDamping: Double = 0.8 static let hoverSpringDamping: Double = 0.8
static let resizeAnimationDuration: Double = 0.42
static let enableGestures: Bool = true static let enableGestures: Bool = true
static let gestureSensitivity: Double = 0.5 static let gestureSensitivity: Double = 0.5
@@ -132,6 +134,7 @@ enum NotchSettings {
Keys.closeSpringDamping: Defaults.closeSpringDamping, Keys.closeSpringDamping: Defaults.closeSpringDamping,
Keys.hoverSpringResponse: Defaults.hoverSpringResponse, Keys.hoverSpringResponse: Defaults.hoverSpringResponse,
Keys.hoverSpringDamping: Defaults.hoverSpringDamping, Keys.hoverSpringDamping: Defaults.hoverSpringDamping,
Keys.resizeAnimationDuration: Defaults.resizeAnimationDuration,
Keys.enableGestures: Defaults.enableGestures, Keys.enableGestures: Defaults.enableGestures,
Keys.gestureSensitivity: Defaults.gestureSensitivity, Keys.gestureSensitivity: Defaults.gestureSensitivity,

View File

@@ -18,6 +18,7 @@ class NotchViewModel: ObservableObject {
@Published var isCloseTransitionActive: Bool = false @Published var isCloseTransitionActive: Bool = false
@Published var suppressHoverOpenUntilHoverExit: Bool = false @Published var suppressHoverOpenUntilHoverExit: Bool = false
@Published var isUserResizing: Bool = false @Published var isUserResizing: Bool = false
@Published var isPresetResizing: Bool = false
let terminalManager = TerminalManager.shared let terminalManager = TerminalManager.shared
@@ -36,6 +37,7 @@ class NotchViewModel: ObservableObject {
@AppStorage(NotchSettings.Keys.openSpringDamping) private var openSpringDamping = NotchSettings.Defaults.openSpringDamping @AppStorage(NotchSettings.Keys.openSpringDamping) private var openSpringDamping = NotchSettings.Defaults.openSpringDamping
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse @AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping @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>? private var closeTransitionTask: Task<Void, Never>?
@@ -45,6 +47,9 @@ class NotchViewModel: ObservableObject {
var closeAnimation: Animation { var closeAnimation: Animation {
.spring(response: closeSpringResponse, dampingFraction: closeSpringDamping) .spring(response: closeSpringResponse, dampingFraction: closeSpringDamping)
} }
var openAnimationDuration: TimeInterval {
max(0.05, resizeAnimationDurationSetting)
}
init(screenUUID: String) { init(screenUUID: String) {
self.screenUUID = screenUUID self.screenUUID = screenUUID
@@ -94,10 +99,16 @@ class NotchViewModel: ObservableObject {
} }
@discardableResult @discardableResult
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize { func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
let clampedSize = clampedOpenSize(proposedSize) let clampedSize = clampedOpenSize(proposedSize)
openWidth = clampedSize.width openWidth = clampedSize.width
openHeight = clampedSize.height openHeight = clampedSize.height
return clampedSize
}
@discardableResult
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
let clampedSize = setStoredOpenSize(proposedSize)
if notchState == .open { if notchState == .open {
notchSize = clampedSize notchSize = clampedSize
} }

View File

@@ -227,6 +227,7 @@ struct AnimationSettingsView: View {
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping @AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse @AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping @AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping
@AppStorage(NotchSettings.Keys.resizeAnimationDuration) private var resizeDuration = NotchSettings.Defaults.resizeAnimationDuration
var body: some View { var body: some View {
Form { Form {
@@ -239,6 +240,9 @@ struct AnimationSettingsView: View {
Section("Hover Animation") { Section("Hover Animation") {
springControls(response: $hoverResponse, damping: $hoverDamping) springControls(response: $hoverResponse, damping: $hoverDamping)
} }
Section("Resize Animation") {
durationControl(duration: $resizeDuration)
}
Section { Section {
Button("Reset to Defaults") { Button("Reset to Defaults") {
openResponse = NotchSettings.Defaults.openSpringResponse openResponse = NotchSettings.Defaults.openSpringResponse
@@ -247,6 +251,7 @@ struct AnimationSettingsView: View {
closeDamping = NotchSettings.Defaults.closeSpringDamping closeDamping = NotchSettings.Defaults.closeSpringDamping
hoverResponse = NotchSettings.Defaults.hoverSpringResponse hoverResponse = NotchSettings.Defaults.hoverSpringResponse
hoverDamping = NotchSettings.Defaults.hoverSpringDamping 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) 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 // MARK: - Terminal