// // WrapBox.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 CAdw // MARK: - Swift Enum Wrappers for C Enums /// Controls how children are justified within each line. public enum JustifyMode { /// No justification. case none /// Children are stretched to fill the line. case fill /// Extra space is distributed evenly between children. case spread var cValue: AdwJustifyMode { switch self { case .none: ADW_JUSTIFY_NONE case .fill: ADW_JUSTIFY_FILL case .spread: ADW_JUSTIFY_SPREAD } } } /// Controls the packing direction. public enum PackDirection { /// Children are packed from start to end. case startToEnd /// Children are packed from end to start. case endToStart var cValue: AdwPackDirection { switch self { case .startToEnd: ADW_PACK_START_TO_END case .endToStart: ADW_PACK_END_TO_START } } } /// Controls the wrapping policy. public enum WrapPolicy { /// Wrapping occurs at the minimum size. case minimum /// Wrapping occurs at the natural size. case natural var cValue: AdwWrapPolicy { switch self { case .minimum: ADW_WRAP_MINIMUM case .natural: ADW_WRAP_NATURAL } } } /// Units for spacing and length properties. public enum LengthUnit { /// Pixels. case px /// Points. case pt /// Scale-independent pixels. case sp var cValue: AdwLengthUnit { switch self { case .px: ADW_LENGTH_UNIT_PX case .pt: ADW_LENGTH_UNIT_PT case .sp: ADW_LENGTH_UNIT_SP } } } // MARK: - WrapBox Widget /// A responsive wrapping container that arranges children in a reflowing grid. /// /// `WrapBox` places its children in a horizontal flow, wrapping to the next /// line when the available width is exhausted. It is backed by `AdwWrapBox` from libadwaita. /// /// Use inside a `ScrollView` to allow shrinking in both axes. /// /// ```swift /// ScrollView { /// WrapBox(items) { item in /// ItemCell(item: item) /// } /// .childSpacing(12) /// .lineSpacing(12) /// .lineHomogeneous(true) /// } /// ``` public struct WrapBox: AdwaitaWidget where Identifier: Hashable { #if exposeGeneratedAppearUpdateFunctions /// Additional update functions for type extensions. public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = [] /// Additional appear functions for type extensions. public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = [] #else /// Additional update functions for type extensions. var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = [] /// Additional appear functions for type extensions. var appearFunctions: [(ViewStorage, WidgetData) -> Void] = [] #endif /// The amount of space between children. var childSpacing: Int? /// The unit for `childSpacing`. var childSpacingUnit: LengthUnit? /// The packing direction. var packDirection: PackDirection? /// The alignment of children within each line (0.0 to 1.0). var align: Float? /// The justification mode. var justify: JustifyMode? /// Whether to justify the last line. var justifyLastLine: Bool? /// The amount of space between lines. var lineSpacing: Int? /// The unit for `lineSpacing`. var lineSpacingUnit: LengthUnit? /// Whether all lines should be the same size. var lineHomogeneous: Bool? /// The natural length of each line. var naturalLineLength: Int? /// The unit for `naturalLineLength`. var naturalLineLengthUnit: LengthUnit? /// Whether to reverse the wrapping direction. var wrapReverse: Bool? /// The wrapping policy. var wrapPolicy: WrapPolicy? /// The dynamic widget elements. var elements: [Element] /// The dynamic widget content. var content: (Element) -> Body /// The dynamic widget identifier key path. var id: KeyPath /// Initialize `WrapBox`. /// - Parameters: /// - elements: The elements to display. /// - id: The key path to the element's identifier. /// - content: A view builder for rendering each element. public init( _ elements: [Element], id: KeyPath, @ViewBuilder content: @escaping (Element) -> Body ) { self.elements = elements self.content = content self.id = id } // MARK: - AdwaitaWidget public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { let storage = ViewStorage(adw_wrap_box_new()?.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 // --- Apply property changes --- if let childSpacing, updateProperties, (storage.previousState as? Self)?.childSpacing != childSpacing { adw_wrap_box_set_child_spacing(widget, childSpacing.cInt) } if let childSpacingUnit, updateProperties, (storage.previousState as? Self)?.childSpacingUnit != childSpacingUnit { adw_wrap_box_set_child_spacing_unit(widget, childSpacingUnit.cValue) } if let packDirection, updateProperties, (storage.previousState as? Self)?.packDirection != packDirection { adw_wrap_box_set_pack_direction(widget, packDirection.cValue) } if let align, updateProperties, (storage.previousState as? Self)?.align != align { adw_wrap_box_set_align(widget, align) } if let justify, updateProperties, (storage.previousState as? Self)?.justify != justify { adw_wrap_box_set_justify(widget, justify.cValue) } if let justifyLastLine, updateProperties, (storage.previousState as? Self)?.justifyLastLine != justifyLastLine { adw_wrap_box_set_justify_last_line(widget, justifyLastLine.cBool) } if let lineSpacing, updateProperties, (storage.previousState as? Self)?.lineSpacing != lineSpacing { adw_wrap_box_set_line_spacing(widget, lineSpacing.cInt) } if let lineSpacingUnit, updateProperties, (storage.previousState as? Self)?.lineSpacingUnit != lineSpacingUnit { adw_wrap_box_set_line_spacing_unit(widget, lineSpacingUnit.cValue) } if let lineHomogeneous, updateProperties, (storage.previousState as? Self)?.lineHomogeneous != lineHomogeneous { adw_wrap_box_set_line_homogeneous(widget, lineHomogeneous.cBool) } if let naturalLineLength, updateProperties, (storage.previousState as? Self)?.naturalLineLength != naturalLineLength { adw_wrap_box_set_natural_line_length(widget, naturalLineLength.cInt) } if let naturalLineLengthUnit, updateProperties, (storage.previousState as? Self)?.naturalLineLengthUnit != naturalLineLengthUnit { adw_wrap_box_set_natural_line_length_unit(widget, naturalLineLengthUnit.cValue) } if let wrapReverse, updateProperties, (storage.previousState as? Self)?.wrapReverse != wrapReverse { adw_wrap_box_set_wrap_reverse(widget, wrapReverse.cBool) } if let wrapPolicy, updateProperties, (storage.previousState as? Self)?.wrapPolicy != wrapPolicy { adw_wrap_box_set_wrap_policy(widget, wrapPolicy.cValue) } // --- Child management --- 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) if let lastPtr = lastChild { adw_wrap_box_reorder_child_after( widget, existingStorage.opaquePointer?.cast(), lastPtr.cast() ) } else { adw_wrap_box_reorder_child_after( widget, existingStorage.opaquePointer?.cast(), nil ) } lastChild = existingStorage.opaquePointer } else { let child = content(element).storage(data: data, type: type) if let lastPtr = lastChild { adw_wrap_box_insert_child_after( widget, child.opaquePointer?.cast(), lastPtr.cast() ) } else { adw_wrap_box_prepend(widget, child.opaquePointer?.cast()) } newContentStorage.append(child) lastChild = child.opaquePointer } } for (_, staleStorage) in oldByID { adw_wrap_box_remove(widget, 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: - Modifier Methods extension WrapBox { /// The amount of space between children. public func childSpacing(_ childSpacing: Int?) -> Self { modify { $0.childSpacing = childSpacing } } /// The unit for `childSpacing`. public func childSpacingUnit(_ childSpacingUnit: LengthUnit?) -> Self { modify { $0.childSpacingUnit = childSpacingUnit } } /// The packing direction. public func packDirection(_ packDirection: PackDirection?) -> Self { modify { $0.packDirection = packDirection } } /// The alignment of children within each line (0.0 to 1.0). public func align(_ align: Float?) -> Self { modify { $0.align = align } } /// The justification mode. public func justify(_ justify: JustifyMode?) -> Self { modify { $0.justify = justify } } /// Whether to justify the last line. public func justifyLastLine(_ justifyLastLine: Bool? = true) -> Self { modify { $0.justifyLastLine = justifyLastLine } } /// The amount of space between lines. public func lineSpacing(_ lineSpacing: Int?) -> Self { modify { $0.lineSpacing = lineSpacing } } /// The unit for `lineSpacing`. public func lineSpacingUnit(_ lineSpacingUnit: LengthUnit?) -> Self { modify { $0.lineSpacingUnit = lineSpacingUnit } } /// Whether all lines should be the same size. public func lineHomogeneous(_ lineHomogeneous: Bool? = true) -> Self { modify { $0.lineHomogeneous = lineHomogeneous } } /// The natural length of each line. public func naturalLineLength(_ naturalLineLength: Int?) -> Self { modify { $0.naturalLineLength = naturalLineLength } } /// The unit for `naturalLineLength`. public func naturalLineLengthUnit(_ naturalLineLengthUnit: LengthUnit?) -> Self { modify { $0.naturalLineLengthUnit = naturalLineLengthUnit } } /// Whether to reverse the wrapping direction. public func wrapReverse(_ wrapReverse: Bool? = true) -> Self { modify { $0.wrapReverse = wrapReverse } } /// The wrapping policy. public func wrapPolicy(_ wrapPolicy: WrapPolicy?) -> Self { modify { $0.wrapPolicy = wrapPolicy } } } // MARK: - Convenience Initializer for Identifiable Elements extension WrapBox where Element: Identifiable, Identifier == Element.ID { /// Initialize `WrapBox` with identifiable elements. /// - Parameters: /// - elements: The identifiable elements to display. /// - content: A view builder for rendering each element. public init( _ elements: [Element], @ViewBuilder content: @escaping (Element) -> Body ) { self.elements = elements self.content = content self.id = \.id } }