import Foundation import CoreGraphics import OnyxDesign /// Die Größen, in denen ein Widget im Panel auftreten darf. public enum WidgetSize: String, Codable, Sendable, CaseIterable, Identifiable { /// 1 × 1 — ein Messwert, ein Ring. case small /// 2 × 1 — Messwert mit Verlauf, kurze Liste. case medium /// 2 × 2 — Mini-Monat, Sensorliste. case large /// 4 × 1 — Medienzeile über die volle Breite. case wide public var id: String { rawValue } public var columnSpan: Int { switch self { case .small: 1 case .medium, .large: 2 case .wide: 4 } } public var rowSpan: Int { switch self { case .small, .medium, .wide: 1 case .large: 2 } } } /// Ein Widget an seinem Platz im Layout, wie der Nutzer es angeordnet hat. public struct WidgetPlacement: Codable, Equatable, Identifiable, Sendable { public let id: UUID public var widgetID: String public var size: WidgetSize public init(id: UUID = UUID(), widgetID: String, size: WidgetSize) { self.id = id self.widgetID = widgetID self.size = size } } public struct GridCell: Hashable, Sendable, CustomStringConvertible { public let row: Int public let column: Int public init(row: Int, column: Int) { self.row = row self.column = column } public var description: String { "(\(row),\(column))" } } /// 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 } public var columnSpan: Int { placement.size.columnSpan } public var rowSpan: Int { placement.size.rowSpan } } /// Setzt Widgets in ein Raster mit fester Spaltenzahl. public enum LayoutEngine { public static let columns = 4 /// Kantenlänge eines 1×1-Feldes. public static let cellSize: CGFloat = 96 /// Ordnet die Widgets in der gespeicherten Reihenfolge ein, jedes an die /// erste Stelle, an die es passt. /// /// Der Reihe nach von links oben zu suchen statt stur hintereinander zu /// setzen ist der ganze Unterschied: neben einem 2×2-Widget bleiben rechts /// zwei 1×1-Plätze frei, und ein Verfahren, das immer nur hinten anfügt, /// lässt sie dauerhaft leer. public static func resolve(_ placements: [WidgetPlacement]) -> [ResolvedPlacement] { var occupied = Set() var resolved: [ResolvedPlacement] = [] for placement in placements { let span = placement.size // Zu breit für das Raster: einpassen statt verwerfen. Ein Widget // verschwinden zu lassen wäre für den Nutzer nicht erklärbar. let columnSpan = min(span.columnSpan, columns) guard let cell = firstFreeCell(columnSpan: columnSpan, rowSpan: span.rowSpan, occupied: occupied) else { continue } for row in cell.row..<(cell.row + span.rowSpan) { for column in cell.column..<(cell.column + columnSpan) { occupied.insert(GridCell(row: row, column: column)) } } resolved.append(ResolvedPlacement(placement: placement, row: cell.row, column: cell.column)) } return resolved } private static func firstFreeCell(columnSpan: Int, rowSpan: Int, occupied: Set) -> GridCell? { let maxRow = (occupied.map(\.row).max() ?? -1) + rowSpan + 1 for row in 0...maxRow { for column in 0...(columns - columnSpan) { let fits = (row..<(row + rowSpan)).allSatisfy { r in (column..<(column + columnSpan)).allSatisfy { c in !occupied.contains(GridCell(row: r, column: c)) } } if fits { return GridCell(row: row, column: column) } } } return nil } /// Die Größe, die das Panel für dieses Layout braucht. public static func panelSize(for resolved: [ResolvedPlacement]) -> CGSize { let spacing = Onyx.Metric.tileSpacing let padding = Onyx.Metric.panelPadding let rows = resolved.map { $0.row + $0.rowSpan }.max() ?? 1 let usedRows = max(rows, 1) return CGSize( width: CGFloat(columns) * cellSize + CGFloat(columns - 1) * spacing + 2 * padding, height: CGFloat(usedRows) * cellSize + CGFloat(usedRows - 1) * spacing + 2 * padding) } /// Der Rahmen eines Widgets innerhalb des Panels, in SwiftUI-Koordinaten /// (Ursprung oben links). public static func frame(for item: ResolvedPlacement, in panelSize: CGSize) -> CGRect { let spacing = Onyx.Metric.tileSpacing let padding = Onyx.Metric.panelPadding let x = padding + CGFloat(item.column) * (cellSize + spacing) let y = padding + CGFloat(item.row) * (cellSize + spacing) let width = CGFloat(item.columnSpan) * cellSize + CGFloat(item.columnSpan - 1) * spacing let height = CGFloat(item.rowSpan) * cellSize + CGFloat(item.rowSpan - 1) * spacing return CGRect(x: x, y: y, width: width, height: height) } }