Files
downterm/Downterm/CommandNotch/Models/NotchOrchestrator.swift

190 lines
6.1 KiB
Swift

import Combine
import SwiftUI
@MainActor
protocol ScreenRegistryType: AnyObject {
func allScreens() -> [ScreenContext]
func screenContext(for id: ScreenID) -> ScreenContext?
func activeScreenID() -> ScreenID?
func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID?
@discardableResult
func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID?
func releaseWorkspacePresentation(for screenID: ScreenID)
}
@MainActor
protocol NotchPresentationHost: AnyObject {
func canPresentNotch(for screenID: ScreenID) -> Bool
func performOpenPresentation(for screenID: ScreenID)
func performClosePresentation(for screenID: ScreenID)
}
protocol SchedulerType {
@MainActor
func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable
}
struct TaskScheduler: SchedulerType {
@MainActor
func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable {
let task = Task { @MainActor in
try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
guard !Task.isCancelled else { return }
action()
}
return AnyCancellable {
task.cancel()
}
}
}
@MainActor
final class NotchOrchestrator {
private let screenRegistry: any ScreenRegistryType
private weak var host: (any NotchPresentationHost)?
private let settingsController: AppSettingsController
private let scheduler: any SchedulerType
private var hoverOpenTasks: [ScreenID: AnyCancellable] = [:]
private var closeTransitionTasks: [ScreenID: AnyCancellable] = [:]
init(
screenRegistry: any ScreenRegistryType,
host: any NotchPresentationHost,
settingsController: AppSettingsController? = nil,
scheduler: (any SchedulerType)? = nil
) {
self.screenRegistry = screenRegistry
self.host = host
self.settingsController = settingsController ?? AppSettingsController.shared
self.scheduler = scheduler ?? TaskScheduler()
}
func toggleOnActiveScreen() {
guard let screenID = screenRegistry.activeScreenID(),
host?.canPresentNotch(for: screenID) == true,
let context = screenRegistry.screenContext(for: screenID) else {
return
}
if context.notchState == .open {
close(screenID: screenID)
} else {
open(screenID: screenID)
}
}
func open(screenID: ScreenID) {
guard host?.canPresentNotch(for: screenID) == true,
let context = screenRegistry.screenContext(for: screenID) else {
return
}
if let presentingScreenID = screenRegistry.presentingScreenID(for: context.workspaceID),
presentingScreenID != screenID {
close(screenID: presentingScreenID)
}
cancelHoverOpen(for: screenID)
cancelCloseTransition(for: screenID)
context.cancelCloseTransition()
withAnimation(context.openAnimation) {
context.open()
}
_ = screenRegistry.claimWorkspacePresentation(for: screenID)
host?.performOpenPresentation(for: screenID)
}
func close(screenID: ScreenID) {
guard let context = screenRegistry.screenContext(for: screenID) else { return }
cancelHoverOpen(for: screenID)
cancelCloseTransition(for: screenID)
context.beginCloseTransition()
closeTransitionTasks[screenID] = scheduler.schedule(after: context.closeInteractionLockDuration) { [weak self] in
self?.finishCloseTransition(for: screenID)
}
withAnimation(context.closeAnimation) {
context.close()
}
screenRegistry.releaseWorkspacePresentation(for: screenID)
host?.performClosePresentation(for: screenID)
}
func handleHoverChange(_ hovering: Bool, for screenID: ScreenID) {
guard let context = screenRegistry.screenContext(for: screenID) else { return }
context.isHovering = hovering
if hovering {
scheduleHoverOpenIfNeeded(for: screenID)
} else {
cancelHoverOpen(for: screenID)
context.clearHoverOpenSuppression()
}
}
func cancelAllPendingWork() {
for task in hoverOpenTasks.values {
task.cancel()
}
for task in closeTransitionTasks.values {
task.cancel()
}
hoverOpenTasks.removeAll()
closeTransitionTasks.removeAll()
}
private func scheduleHoverOpenIfNeeded(for screenID: ScreenID) {
cancelHoverOpen(for: screenID)
guard let context = screenRegistry.screenContext(for: screenID) else { return }
guard settingsController.settings.behavior.openNotchOnHover,
context.notchState == .closed,
!context.isCloseTransitionActive,
!context.suppressHoverOpenUntilHoverExit,
context.isHovering else {
return
}
hoverOpenTasks[screenID] = scheduler.schedule(after: settingsController.settings.behavior.minimumHoverDuration) { [weak self] in
guard let self,
let context = self.screenRegistry.screenContext(for: screenID),
context.isHovering,
context.notchState == .closed,
!context.isCloseTransitionActive,
!context.suppressHoverOpenUntilHoverExit else {
return
}
self.hoverOpenTasks[screenID] = nil
self.open(screenID: screenID)
}
}
private func finishCloseTransition(for screenID: ScreenID) {
closeTransitionTasks[screenID] = nil
guard let context = screenRegistry.screenContext(for: screenID) else { return }
context.endCloseTransition()
scheduleHoverOpenIfNeeded(for: screenID)
}
private func cancelHoverOpen(for screenID: ScreenID) {
hoverOpenTasks[screenID]?.cancel()
hoverOpenTasks[screenID] = nil
}
private func cancelCloseTransition(for screenID: ScreenID) {
closeTransitionTasks[screenID]?.cancel()
closeTransitionTasks[screenID] = nil
}
}