import CoreGraphics /// Die Bildschirmwerte, aus denen sich die Notch ableiten lässt — als eigener /// Typ, damit die Geometrie ohne echten `NSScreen` prüfbar bleibt. public struct ScreenMetrics: Equatable, Sendable, Identifiable { public let frame: CGRect public let safeAreaTop: CGFloat public let auxiliaryTopLeft: CGRect? public let auxiliaryTopRight: CGRect? public let isBuiltIn: Bool /// `CGDirectDisplayID` — stabil über Anordnungswechsel hinweg, anders als /// der Index in `NSScreen.screens`. public let displayID: UInt32 public var id: UInt32 { displayID } public init(frame: CGRect, safeAreaTop: CGFloat, auxiliaryTopLeft: CGRect?, auxiliaryTopRight: CGRect?, isBuiltIn: Bool = false, displayID: UInt32 = 0) { self.frame = frame self.safeAreaTop = safeAreaTop self.auxiliaryTopLeft = auxiliaryTopLeft self.auxiliaryTopRight = auxiliaryTopRight self.isBuiltIn = isBuiltIn self.displayID = displayID } } /// Auf welchen Displays Onyx überhaupt erscheint. public enum DisplayPolicy: String, Codable, Sendable, CaseIterable { /// Nur das eingebaute Display. case builtInOnly /// Jedes angeschlossene Display, externe mit virtueller Notch. case allDisplays public func screens(from available: [ScreenMetrics]) -> [ScreenMetrics] { switch self { case .allDisplays: return available case .builtInOnly: let builtIn = available.filter(\.isBuiltIn) // Am Dock mit geschlossenem Deckel gibt es kein eingebautes Display. // Diese Einstellung beim Wort zu nehmen hieße, Onyx unerreichbar zu // machen — deshalb ein einzelnes Ersatzdisplay statt gar keinem. guard builtIn.isEmpty else { return builtIn } return Array(available.prefix(1)) } } } /// Wo die Notch sitzt und wo das Panel dazu hingehört. /// /// Auf dem eingebauten Display wird die echte Notch aus den beiden Hilfsflächen /// links und rechts davon berechnet. Auf allen anderen Displays tritt ein /// virtueller Ersatz an dieselbe Stelle, damit Onyx dort überhaupt erreichbar ist. public struct NotchGeometry: Equatable, Sendable { public enum Kind: Equatable, Sendable { /// Aus `auxiliaryTopLeftArea` / `auxiliaryTopRightArea` abgeleitet. case physical /// Frei gesetzter Balken oben mittig. case virtual } public let kind: Kind /// Die Notch selbst, in Bildschirmkoordinaten (Ursprung unten links). public let rect: CGRect public let screenFrame: CGRect /// Wie weit die Auslösefläche über die Notch hinausreicht. Ohne diesen Rand /// muss man die Notch pixelgenau treffen, und die unteren Bildpunkte der /// Notch selbst nimmt die Menüleiste bereits für sich in Anspruch. public static let triggerInsetX: CGFloat = 16 public static let triggerInsetY: CGFloat = 4 public init(kind: Kind, rect: CGRect, screenFrame: CGRect) { self.kind = kind self.rect = rect self.screenFrame = screenFrame } public static func resolve(_ metrics: ScreenMetrics, virtualSize: CGSize) -> NotchGeometry { // Eine echte Notch erkennt man an den beiden Flächen daneben, nicht am // Inset allein: bei Spiegelung und Fernanzeige ist `safeAreaTop` gesetzt, // obwohl es nichts gibt, um das man herumlegen könnte. if let left = metrics.auxiliaryTopLeft, let right = metrics.auxiliaryTopRight, metrics.safeAreaTop > 0 { let width = metrics.frame.width - left.width - right.width if width > 0 { return NotchGeometry( kind: .physical, rect: CGRect(x: left.maxX, y: metrics.frame.maxY - metrics.safeAreaTop, width: width, height: metrics.safeAreaTop), screenFrame: metrics.frame) } } let width = min(virtualSize.width, metrics.frame.width) return NotchGeometry( kind: .virtual, rect: CGRect(x: metrics.frame.midX - width / 2, y: metrics.frame.maxY - virtualSize.height, width: width, height: virtualSize.height), screenFrame: metrics.frame) } /// Die Fläche, deren Betreten das Panel scharf schaltet. public var triggerRect: CGRect { rect .insetBy(dx: -Self.triggerInsetX, dy: -Self.triggerInsetY) .clamped(to: screenFrame) } /// Wohin das Panel gehört: auf die Notch zentriert, oben bündig mit dem /// Bildschirmrand — es wächst aus der Notch heraus, statt darunter zu schweben. public func panelRect(for size: CGSize) -> CGRect { let width = min(size.width, screenFrame.width) let height = min(size.height, screenFrame.height) return CGRect(x: rect.midX - width / 2, y: screenFrame.maxY - height, width: width, height: height) .clamped(to: screenFrame) } } extension CGRect { /// Schiebt das Rechteck in die Grenzen hinein, statt es abzuschneiden. /// Ein zurechtgestutztes Panel wäre halb unsichtbar; ein verschobenes ist /// vollständig da. func clamped(to bounds: CGRect) -> CGRect { let width = Swift.min(width, bounds.width) let height = Swift.min(height, bounds.height) let x = Swift.min(Swift.max(minX, bounds.minX), bounds.maxX - width) let y = Swift.min(Swift.max(minY, bounds.minY), bounds.maxY - height) return CGRect(x: x, y: y, width: width, height: height) } }