97 lines
3.3 KiB
Swift
97 lines
3.3 KiB
Swift
import SwiftUI
|
|
import Combine
|
|
|
|
/// Per-screen observable state that drives the notch UI.
|
|
@MainActor
|
|
class NotchViewModel: ObservableObject {
|
|
|
|
let screenUUID: String
|
|
|
|
@Published var notchState: NotchState = .closed
|
|
@Published var notchSize: CGSize
|
|
@Published var closedNotchSize: CGSize
|
|
@Published var isHovering: Bool = false
|
|
@Published var isCloseTransitionActive: Bool = false
|
|
|
|
let terminalManager = TerminalManager.shared
|
|
|
|
/// Set by ScreenManager — routes open/close through proper
|
|
/// window activation so the terminal receives keyboard input.
|
|
var requestOpen: (() -> Void)?
|
|
var requestClose: (() -> Void)?
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
|
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
|
|
|
@AppStorage(NotchSettings.Keys.openSpringResponse) private var openSpringResponse = NotchSettings.Defaults.openSpringResponse
|
|
@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
|
|
|
|
private var closeTransitionTask: Task<Void, Never>?
|
|
|
|
var openAnimation: Animation {
|
|
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
|
|
}
|
|
var closeAnimation: Animation {
|
|
.spring(response: closeSpringResponse, dampingFraction: closeSpringDamping)
|
|
}
|
|
|
|
init(screenUUID: String) {
|
|
self.screenUUID = screenUUID
|
|
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
|
let closed = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
|
self.closedNotchSize = closed
|
|
self.notchSize = closed
|
|
}
|
|
|
|
func open() {
|
|
notchSize = CGSize(width: openWidth, height: openHeight)
|
|
notchState = .open
|
|
}
|
|
|
|
func close() {
|
|
refreshClosedSize()
|
|
notchSize = closedNotchSize
|
|
notchState = .closed
|
|
}
|
|
|
|
func refreshClosedSize() {
|
|
let screen = NSScreen.screens.first { $0.displayUUID == screenUUID } ?? NSScreen.main
|
|
closedNotchSize = screen?.closedNotchSize() ?? CGSize(width: 220, height: 32)
|
|
}
|
|
|
|
var openNotchSize: CGSize {
|
|
CGSize(width: openWidth, height: openHeight)
|
|
}
|
|
|
|
var closeInteractionLockDuration: TimeInterval {
|
|
max(closeSpringResponse + 0.2, 0.35)
|
|
}
|
|
|
|
func beginCloseTransition() {
|
|
closeTransitionTask?.cancel()
|
|
isCloseTransitionActive = true
|
|
|
|
let delay = closeInteractionLockDuration
|
|
closeTransitionTask = Task { @MainActor [weak self] in
|
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
|
guard let self, !Task.isCancelled else { return }
|
|
self.isCloseTransitionActive = false
|
|
self.closeTransitionTask = nil
|
|
}
|
|
}
|
|
|
|
func cancelCloseTransition() {
|
|
closeTransitionTask?.cancel()
|
|
closeTransitionTask = nil
|
|
isCloseTransitionActive = false
|
|
}
|
|
|
|
deinit {
|
|
closeTransitionTask?.cancel()
|
|
}
|
|
}
|