// // FlowGrid.swift // // Copyright 2026 Brendan Szymanski // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later // import Adwaita import CModules /// Controls how the last row is aligned when it doesn't fill the width. public enum FlowGridJustifySetting: Int { /// Last row items are aligned to the start. case start /// Last row items are centered. case center /// Last row items are aligned to the end. case end var cValue: FlowGridJustify { switch self { case .start: FLOW_GRID_JUSTIFY_START case .center: FLOW_GRID_JUSTIFY_CENTER case .end: FLOW_GRID_JUSTIFY_END } } } /// A reflowing grid that arranges children in columns. /// /// Unlike `GtkFlowBox`, this widget re‑measures children during every layout /// pass using the actual column width. This avoids stale size caches and /// ensures the `AspectContainer` receives the correct `for_size` for its /// height computation. /// /// ```swift /// FlowGrid(items, id: \.id) { item in /// HomePosterCell(item: item, navigation: $navigation) /// } /// .columnSpacing(16) /// .rowSpacing(16) /// .halign(.fill) /// ``` public struct FlowGrid: Widget where Identifier: Hashable { #if exposeGeneratedAppearUpdateFunctions public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = [] public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = [] #else var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = [] var appearFunctions: [(ViewStorage, WidgetData) -> Void] = [] #endif /// The minimum column width. var minimumSize: Int = 200 /// The spacing between columns. var columnSpacing: Int = 0 /// The spacing between rows. var rowSpacing: Int = 0 /// How the last row is aligned. var justify: FlowGridJustifySetting = .start /// The elements to display. var elements: [Element] /// The content builder. var content: (Element) -> Body /// The identifier key path. var id: KeyPath /// Initialize `FlowGrid`. public init( _ elements: [Element], id: KeyPath, @ViewBuilder content: @escaping (Element) -> Body ) { self.elements = elements self.content = content self.id = id } // MARK: - Widget public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { let storage = ViewStorage( flow_grid_new(minimumSize.cInt, columnSpacing.cInt, rowSpacing.cInt)?.opaque() ) for function in appearFunctions { function(storage, data) } return storage } public func update( _ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type ) where Data: ViewRenderData { storage.modify { widget in // --- Properties --- if updateProperties, (storage.previousState as? Self)?.minimumSize != minimumSize { flow_grid_set_minimum_size(widget, minimumSize.cInt) } if updateProperties, (storage.previousState as? Self)?.columnSpacing != columnSpacing { flow_grid_set_column_spacing(widget, columnSpacing.cInt) } if updateProperties, (storage.previousState as? Self)?.rowSpacing != rowSpacing { flow_grid_set_row_spacing(widget, rowSpacing.cInt) } if updateProperties, (storage.previousState as? Self)?.justify != justify { flow_grid_set_justify(widget, justify.cValue) } // --- Children --- var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? [] let oldElements = storage.fields["element"] as? [Element] ?? [] var oldByID: [Identifier: ViewStorage] = [:] for (i, oldElement) in oldElements.enumerated() where i < contentStorage.count { oldByID[oldElement[keyPath: id]] = contentStorage[i] } var newContentStorage: [ViewStorage] = [] var lastChild: OpaquePointer? for element in elements { let elementID = element[keyPath: id] if let existingStorage = oldByID.removeValue(forKey: elementID) { newContentStorage.append(existingStorage) lastChild = existingStorage.opaquePointer } else { let child = content(element).storage(data: data, type: type) gtk_widget_set_parent(child.opaquePointer?.cast(), widget?.cast()) newContentStorage.append(child) lastChild = child.opaquePointer } } for (_, staleStorage) in oldByID { gtk_widget_unparent(staleStorage.opaquePointer?.cast()) } storage.fields["element"] = elements storage.content[.mainContent] = newContentStorage for (index, element) in elements.enumerated() { content(element).updateStorage( newContentStorage[index], data: data, updateProperties: updateProperties, type: type ) } } for function in updateFunctions { function(storage, data, updateProperties) } if updateProperties { storage.previousState = self } } } // MARK: - Modifiers extension FlowGrid { /// The minimum column width in pixels. public func minimumSize(_ minimumSize: Int) -> Self { modify { $0.minimumSize = minimumSize } } /// The spacing between columns in pixels. public func columnSpacing(_ columnSpacing: Int) -> Self { modify { $0.columnSpacing = columnSpacing } } /// The spacing between rows in pixels. public func rowSpacing(_ rowSpacing: Int) -> Self { modify { $0.rowSpacing = rowSpacing } } /// How the last row is aligned. public func justify(_ justify: FlowGridJustifySetting) -> Self { modify { $0.justify = justify } } } // MARK: - Convenience Initializer for Identifiable Elements extension FlowGrid where Element: Identifiable, Identifier == Element.ID { /// Initialize `FlowGrid` with identifiable elements. public init( _ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body ) { self.elements = elements self.content = content self.id = \.id } }