Improve animations and resizing again. Add option for animation speed.
This commit is contained in:
Binary file not shown.
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user