// SPDX-License-Identifier: MIT // Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors import SwiftUI /// Flows chips into rows, starting a new row when the next chip does not fit. nonisolated struct FilterChipGroupFlowLayout: Layout { var spacing: CGFloat = 8 /// The indices of the items in each row. An item wider than the row gets a row of its own. static func rows(for widths: [CGFloat], maxWidth: CGFloat, spacing: CGFloat) -> [[Int]] { var rows: [[Int]] = [] var current: [Int] = [] var used: CGFloat = 0 for (index, width) in widths.enumerated() { let needed = current.isEmpty ? width : used + spacing + width if needed > maxWidth, !current.isEmpty { rows.append(current) current = [index] used = width } else { current.append(index) used = needed } } if !current.isEmpty { rows.append(current) } return rows } /// Measures each chip at no more than the row, so a long one truncates instead of overflowing. private func sizes(of subviews: Subviews, maxWidth: CGFloat) -> [CGSize] { subviews.map { subview in let natural = subview.sizeThatFits(.unspecified) return natural.width > maxWidth ? subview.sizeThatFits(ProposedViewSize(width: maxWidth, height: nil)) : natural } } func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let maxWidth = proposal.width ?? .infinity let sizes = sizes(of: subviews, maxWidth: maxWidth) var width: CGFloat = 0 var height: CGFloat = 0 for (number, row) in Self.rows(for: sizes.map(\.width), maxWidth: maxWidth, spacing: spacing).enumerated() { let rowWidths = row.map { sizes[$0].width } let rowWidth = rowWidths.reduce(0, +) + spacing * CGFloat(row.count - 1) let rowHeight = row.map { sizes[$0].height }.max() ?? 0 width = max(width, rowWidth) height += number == 0 ? rowHeight : spacing + rowHeight } return CGSize(width: min(width, maxWidth), height: height) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let sizes = sizes(of: subviews, maxWidth: bounds.width) var y = bounds.minY for row in Self.rows(for: sizes.map(\.width), maxWidth: bounds.width, spacing: spacing) { var x = bounds.minX let rowHeight = row.map { sizes[$0].height }.max() ?? 0 for index in row { subviews[index].place(at: CGPoint(x: x, y: y + rowHeight / 2), anchor: .leading, proposal: ProposedViewSize(sizes[index])) x += sizes[index].width + spacing } y += rowHeight + spacing } } } /// Chips that filter a list: each one toggles on its own, shows a checkmark /// when selected, and the group wraps onto as many rows as it needs. /// /// With Reduce Motion on, the checkmark fades in instead of growing. struct FilterChipGroup: View { let options: [Option] @Binding var selection: Set