Yep. AI rewrote the whole thing.
This commit is contained in:
222
Downterm/CommandNotch/Models/ScreenContext.swift
Normal file
222
Downterm/CommandNotch/Models/ScreenContext.swift
Normal file
@@ -0,0 +1,222 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
typealias ScreenID = String
|
||||
|
||||
/// Observable screen-local UI state for one physical display.
|
||||
@MainActor
|
||||
final class ScreenContext: ObservableObject, Identifiable {
|
||||
private static let minimumOpenWidth: CGFloat = 320
|
||||
private static let minimumOpenHeight: CGFloat = 140
|
||||
private static let windowHorizontalPadding: CGFloat = 40
|
||||
private static let windowVerticalPadding: CGFloat = 20
|
||||
|
||||
let id: ScreenID
|
||||
|
||||
@Published var workspaceID: WorkspaceID
|
||||
@Published var notchState: NotchState = .closed
|
||||
@Published var notchSize: CGSize
|
||||
@Published var closedNotchSize: CGSize
|
||||
@Published var isHovering = false
|
||||
@Published var isCloseTransitionActive = false
|
||||
@Published var suppressHoverOpenUntilHoverExit = false
|
||||
@Published var isUserResizing = false
|
||||
@Published var isPresetResizing = false
|
||||
@Published private(set) var suppressCloseOnFocusLoss = false
|
||||
|
||||
var requestOpen: (() -> Void)?
|
||||
var requestClose: (() -> Void)?
|
||||
var requestWindowResize: (() -> Void)?
|
||||
var requestTerminalFocus: (() -> Void)?
|
||||
|
||||
private let settingsController: AppSettingsController
|
||||
private let screenProvider: @MainActor (ScreenID) -> NSScreen?
|
||||
|
||||
init(
|
||||
id: ScreenID,
|
||||
workspaceID: WorkspaceID,
|
||||
settingsController: AppSettingsController? = nil,
|
||||
screenProvider: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
|
||||
NSScreen.screens.first { $0.displayUUID == screenID }
|
||||
}
|
||||
) {
|
||||
self.id = id
|
||||
self.workspaceID = workspaceID
|
||||
self.settingsController = settingsController ?? AppSettingsController.shared
|
||||
self.screenProvider = screenProvider
|
||||
|
||||
let closed = Self.resolveClosedNotchSize(
|
||||
for: id,
|
||||
using: self.settingsController.settings.display,
|
||||
screenProvider: screenProvider
|
||||
)
|
||||
self.closedNotchSize = closed
|
||||
self.notchSize = closed
|
||||
}
|
||||
|
||||
var openAnimation: Animation {
|
||||
let animation = settingsController.settings.animation
|
||||
return .spring(
|
||||
response: animation.openSpringResponse,
|
||||
dampingFraction: animation.openSpringDamping
|
||||
)
|
||||
}
|
||||
|
||||
var closeAnimation: Animation {
|
||||
let animation = settingsController.settings.animation
|
||||
return .spring(
|
||||
response: animation.closeSpringResponse,
|
||||
dampingFraction: animation.closeSpringDamping
|
||||
)
|
||||
}
|
||||
|
||||
var openAnimationDuration: TimeInterval {
|
||||
max(0.05, settingsController.settings.animation.resizeAnimationDuration)
|
||||
}
|
||||
|
||||
func open() {
|
||||
notchSize = openNotchSize
|
||||
notchState = .open
|
||||
}
|
||||
|
||||
func close() {
|
||||
refreshClosedSize()
|
||||
notchSize = closedNotchSize
|
||||
notchState = .closed
|
||||
}
|
||||
|
||||
func updateWorkspace(id: WorkspaceID) {
|
||||
guard workspaceID != id else { return }
|
||||
workspaceID = id
|
||||
}
|
||||
|
||||
func refreshClosedSize() {
|
||||
closedNotchSize = Self.resolveClosedNotchSize(
|
||||
for: id,
|
||||
using: settingsController.settings.display,
|
||||
screenProvider: screenProvider
|
||||
)
|
||||
}
|
||||
|
||||
var openNotchSize: CGSize {
|
||||
let display = settingsController.settings.display
|
||||
return clampedOpenSize(
|
||||
CGSize(width: display.openWidth, height: display.openHeight)
|
||||
)
|
||||
}
|
||||
|
||||
func beginInteractiveResize() {
|
||||
isUserResizing = true
|
||||
}
|
||||
|
||||
func resizeOpenNotch(to proposedSize: CGSize) {
|
||||
let clampedSize = clampedOpenSize(proposedSize)
|
||||
if notchState == .open {
|
||||
notchSize = clampedSize
|
||||
}
|
||||
requestWindowResize?()
|
||||
}
|
||||
|
||||
func endInteractiveResize() {
|
||||
if notchState == .open {
|
||||
settingsController.update {
|
||||
$0.display.openWidth = notchSize.width
|
||||
$0.display.openHeight = notchSize.height
|
||||
}
|
||||
}
|
||||
isUserResizing = false
|
||||
}
|
||||
|
||||
func applySizePreset(_ preset: TerminalSizePreset, notifyWindowResize: Bool = true) {
|
||||
setOpenSize(preset.size, notifyWindowResize: notifyWindowResize)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
|
||||
let clampedSize = clampedOpenSize(proposedSize)
|
||||
settingsController.update {
|
||||
$0.display.openWidth = clampedSize.width
|
||||
$0.display.openHeight = clampedSize.height
|
||||
}
|
||||
return clampedSize
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
|
||||
let clampedSize = setStoredOpenSize(proposedSize)
|
||||
if notchState == .open {
|
||||
notchSize = clampedSize
|
||||
}
|
||||
if notifyWindowResize {
|
||||
requestWindowResize?()
|
||||
}
|
||||
return clampedSize
|
||||
}
|
||||
|
||||
private func clampedOpenSize(_ size: CGSize) -> CGSize {
|
||||
CGSize(
|
||||
width: size.width.clamped(to: Self.minimumOpenWidth...maximumAllowedWidth),
|
||||
height: size.height.clamped(to: Self.minimumOpenHeight...maximumAllowedHeight)
|
||||
)
|
||||
}
|
||||
|
||||
private var maximumAllowedWidth: CGFloat {
|
||||
guard let screen = resolvedScreen() ?? NSScreen.main else {
|
||||
return Self.minimumOpenWidth
|
||||
}
|
||||
return max(Self.minimumOpenWidth, screen.frame.width - Self.windowHorizontalPadding)
|
||||
}
|
||||
|
||||
private var maximumAllowedHeight: CGFloat {
|
||||
guard let screen = resolvedScreen() ?? NSScreen.main else {
|
||||
return Self.minimumOpenHeight
|
||||
}
|
||||
return max(Self.minimumOpenHeight, screen.frame.height - Self.windowVerticalPadding)
|
||||
}
|
||||
|
||||
var closeInteractionLockDuration: TimeInterval {
|
||||
max(settingsController.settings.animation.closeSpringResponse + 0.2, 0.35)
|
||||
}
|
||||
|
||||
func beginCloseTransition() {
|
||||
isCloseTransitionActive = true
|
||||
if isHovering {
|
||||
suppressHoverOpenUntilHoverExit = true
|
||||
}
|
||||
}
|
||||
|
||||
func cancelCloseTransition() {
|
||||
isCloseTransitionActive = false
|
||||
}
|
||||
|
||||
func endCloseTransition() {
|
||||
isCloseTransitionActive = false
|
||||
}
|
||||
|
||||
func clearHoverOpenSuppression() {
|
||||
suppressHoverOpenUntilHoverExit = false
|
||||
}
|
||||
|
||||
func setCloseOnFocusLossSuppressed(_ suppressed: Bool) {
|
||||
suppressCloseOnFocusLoss = suppressed
|
||||
}
|
||||
|
||||
private func resolvedScreen() -> NSScreen? {
|
||||
screenProvider(id)
|
||||
}
|
||||
|
||||
private static func resolveClosedNotchSize(
|
||||
for screenID: ScreenID,
|
||||
using settings: AppSettings.DisplaySettings,
|
||||
screenProvider: @escaping @MainActor (ScreenID) -> NSScreen?
|
||||
) -> CGSize {
|
||||
let screen = screenProvider(screenID) ?? NSScreen.main
|
||||
return screen?.closedNotchSize(using: settings) ?? CGSize(width: 220, height: 32)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
|
||||
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user