luminate-old/Sources/LuminateUI/Components/FlowGrid.swift

232 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// FlowGrid.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// 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 <https://www.gnu.org/licenses/>.
//
// 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 remeasures 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<Element, Identifier>: 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<Element, Identifier>
/// Initialize `FlowGrid`.
public init(
_ elements: [Element],
id: KeyPath<Element, Identifier>,
@ViewBuilder content: @escaping (Element) -> Body
) {
self.elements = elements
self.content = content
self.id = id
}
// MARK: - Widget
public func container<Data>(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<Data>(
_ 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
}
}