190 lines
6.1 KiB
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
|
|
}
|
|
}
|