import SwiftUI import OnyxDesign /// Zeichnet ein aufgelöstes Layout. /// /// Die Positionen kommen aus `LayoutEngine`, nicht aus einem SwiftUI-Grid. Ein /// `LazyVGrid` kann keine Kacheln über zwei Zeilen führen, und genau das braucht /// der Mini-Monat. public struct WidgetGrid: View { private let resolved: [ResolvedPlacement] private let panelSize: CGSize public init(placements: [WidgetPlacement], availableWidth: CGFloat) { self.resolved = LayoutEngine.resolve(placements, availableWidth: availableWidth) self.panelSize = LayoutEngine.panelSize(for: placements, availableWidth: availableWidth) } public var body: some View { ZStack(alignment: .topLeading) { ForEach(resolved) { item in let frame = LayoutEngine.frame(for: item) widgetView(for: item) .frame(width: frame.width, height: frame.height) // Beschneiden ist Pflicht, kein Feinschliff: quillt der // Inhalt über, zeichnet er über die Nachbarkachel — und // ganz oben über die Notch. .clipped() .offset(x: frame.minX, y: frame.minY) } } .frame(width: panelSize.width, height: panelSize.height, alignment: .topLeading) } @ViewBuilder private func widgetView(for item: ResolvedPlacement) -> some View { if let widget = WidgetRegistry.shared.widget(id: item.placement.widgetID) { OnyxTile { widget.makeView() } } else { // Sollte nach dem Filtern in LayoutStore nicht vorkommen. Falls doch, // ist eine sichtbare Lücke mit Namen besser als ein stiller Ausfall. OnyxTile { VStack(spacing: 4) { Image(systemName: "questionmark.square.dashed") Text(item.placement.widgetID) .font(Onyx.Font.caption) } .foregroundStyle(Onyx.Color.textTertiary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } /// Die Größe, die das Panel für dieses Layout braucht — der Controller /// stellt das Fenster darauf ein. public static func panelSize(for placements: [WidgetPlacement], availableWidth: CGFloat) -> CGSize { LayoutEngine.panelSize(for: placements, availableWidth: availableWidth) } }