import Foundation import CoreGraphics import OnyxDesign /// Ein Widget an seinem Platz im Layout, in der Reihenfolge, die der Nutzer /// festgelegt hat. /// /// Ohne Größe: alle Kacheln sind gleich groß. Eine Größenauswahl klingt nach /// Freiheit und bringt vor allem Löcher — ein 2×1 neben einem 2×2 lässt oben /// rechts eine Lücke, die niemand füllen kann und die aussieht wie ein Fehler. public struct WidgetPlacement: Codable, Equatable, Identifiable, Sendable { public let id: UUID public var widgetID: String public init(id: UUID = UUID(), widgetID: String) { self.id = id self.widgetID = widgetID } /// Ältere Layouts trugen noch eine Größe. Sie wird beim Lesen verworfen, /// statt die Datei für ungültig zu erklären — sonst stünde nach dem /// Aktualisieren ein leeres Panel da. private enum CodingKeys: String, CodingKey { case id, widgetID } } /// Ein Widget mit ausgerechneter Rasterposition. public struct ResolvedPlacement: Equatable, Identifiable, Sendable { public let placement: WidgetPlacement public let row: Int public let column: Int public var id: UUID { placement.id } } /// Setzt Widgets nebeneinander. /// /// Das Panel wächst in die **Breite**. Es sitzt an der Notch, also am oberen /// Rand — nach unten zu wachsen bedeckt den Bildschirm, nach rechts und links /// legt es sich in den ohnehin leeren Streifen neben der Notch. Eine zweite /// Reihe entsteht erst, wenn der Bildschirm keine weitere Kachel mehr hergibt. public enum LayoutEngine { /// Die eine Kachelgröße. /// /// Hochkant, weil fast jedes Widget eine Liste zeigt: Termine, Sensoren, /// Programme im Mischpult. Breiter als hoch wäre für ein einziges davon /// besser und für alle anderen schlechter. public static let tileSize = CGSize(width: 180, height: 200) public static var tileSpacing: CGFloat { Onyx.Metric.tileSpacing } private static var padding: CGFloat { Onyx.Metric.panelPadding } /// Wie viele Kacheln nebeneinander passen. public static func columns(availableWidth: CGFloat) -> Int { let usable = availableWidth - 2 * padding + tileSpacing let fitting = Int(usable / (tileSize.width + tileSpacing)) // Mindestens eine: lieber ein Panel, das über den Rand ragt, als eine // Division durch null oder ein Panel ganz ohne Inhalt. return max(1, fitting) } public static func resolve(_ placements: [WidgetPlacement], availableWidth: CGFloat) -> [ResolvedPlacement] { let columnCount = columns(availableWidth: availableWidth) return placements.enumerated().map { index, placement in ResolvedPlacement(placement: placement, row: index / columnCount, column: index % columnCount) } } /// Die Größe, die das Panel für dieses Layout braucht. public static func panelSize(for placements: [WidgetPlacement], availableWidth: CGFloat) -> CGSize { let columnCount = columns(availableWidth: availableWidth) // Ein leeres Layout bekommt trotzdem eine Kachelfläche: ein Panel ohne // Ausdehnung wäre ein unsichtbares Panel, und der Nutzer hielte das // für einen Absturz. let count = max(placements.count, 1) let used = min(count, columnCount) let rows = Int(ceil(Double(count) / Double(columnCount))) return CGSize( width: CGFloat(used) * tileSize.width + CGFloat(used - 1) * tileSpacing + 2 * padding, height: CGFloat(rows) * tileSize.height + CGFloat(rows - 1) * tileSpacing + 2 * padding) } /// Der Rahmen einer Kachel im Panel, in SwiftUI-Koordinaten (oben links). public static func frame(for item: ResolvedPlacement) -> CGRect { CGRect(x: padding + CGFloat(item.column) * (tileSize.width + tileSpacing), y: padding + CGFloat(item.row) * (tileSize.height + tileSpacing), width: tileSize.width, height: tileSize.height) } }