import Gtk /// A view that renders one row per element of a reactive array, keyed by a /// stable identifier, and applies a keyed diff on change - appending new rows, /// removing deleted rows, and reordering moved rows. Surviving rows are never /// rebuilt, so widget identity and subscriptions are preserved. /// /// When placed in a dynamic ordered container's `children:` builder, rows spread /// directly into that container. Supported parents are `Gtk.Box`, `HStack`, /// `VStack`, `Gtk.ListBox`, `Gtk.FlowBox`, `Adw.WrapBox`, and `Adw.Carousel`. /// In a single-widget slot, modifier closure, unsupported container, or after a /// non-`.environment` modifier, it falls back to its own vertical `Gtk.Box`. /// Reactivity requires a state-backed `Binding` (for example, `$items`); a custom /// `Binding(get:set:)` does not track and will not update. Rows are built from an /// element value snapshot at insert time. @MainActor public struct ForEach: View { private let data: Binding<[Element]> private let id: KeyPath private let row: (Element) -> [AnyView] public var body: Never { fatalError() } /// Creates a keyed reactive collection of row views. /// /// - Parameters: /// - data: The reactive array driving the rows. /// - id: Key path to each element's stable identifier. IDs MUST be unique; /// on a duplicate the first occurrence wins and later ones are ignored /// for that diff. /// - row: Builds one row's views from an element value snapshot. public init( _ data: Binding<[Element]>, id: KeyPath, @ViewBuilder row: @escaping (Element) -> [AnyView] ) { self.data = data self.id = id self.row = row } } @_spi(Portico) extension ForEach: DynamicViewGroup { @_spi(Portico) public func mountRegion(_ region: ChildRegion, ctx: MountContext) { // Per-key on-screen state persists across diffs in the tracker closure. var rows: [ID: (widgets: [Gtk.Widget], registry: NodeRegistry)] = [:] var order: [ID] = [] /// Position of each key in `order`, so a moved key is located in O(1) /// instead of by a linear `order.firstIndex(of:)` scan. var indexOf: [ID: Int] = [:] /// Region-local widget offset of each ordered row. The trailing element /// stores the region's total widget count, so every valid row position /// and the insertion tail are answered in O(1). var offsets: [Int] = [0] /// Restores key positions and widget offsets after an array mutation. /// The rebuilt suffix is no longer than the array memmove that caused /// the mutation, so it adds no asymptotic cost. func rebuildTail(from position: Int) { var offset = offsets[position] for i in position..() target.reserveCapacity(source.count) for (index, element) in source.enumerated() { let key = element[keyPath: id] if target.insert(key).inserted { ordered.append((key, index)) } } // Detach removed rows and tear down only their child registries. var position = 0 while position < order.count { let key = order[position] if target.contains(key) { position += 1 continue } let entry = rows[key]! let offset = offsets[position] for _ in entry.widgets.indices { region.remove(at: offset) } entry.registry.teardown() ctx.registry.removeChild(entry.registry) rows[key] = nil order.remove(at: position) indexOf.removeValue(forKey: key) offsets.removeLast() rebuildTail(from: position) } // Place rows left to right. The processed prefix already matches the // target prefix, so local offsets are valid for each operation. for (position, entry) in ordered.enumerated() { let key = entry.key if position < order.count && order[position] == key { continue } if let current = indexOf[key] { let count = rows[key]!.widgets.count region.move(from: offsets[current], count: count, to: offsets[position]) order.remove(at: current) order.insert(key, at: position) rebuildTail(from: position) } else { let child = ctx.makeChild() let widgets = row(source[entry.source]).map { $0.makeWidget(child) } var offset = offsets[position] for widget in widgets { region.insert(widget, at: offset) offset += 1 } rows[key] = (widgets, child.registry) order.insert(key, at: position) offsets.append(0) rebuildTail(from: position) } } } let tracker = DependencyTracker(observation: .disabled) { applyDiff() } tracker.run() ctx.registry.add(tracker) } } @_spi(Portico) extension ForEach: Mountable { /// Mounts rows into a vertical box when no dynamic parent can spread them. @_spi(Portico) public func mount(_ ctx: MountContext) -> Gtk.Widget { let box = Gtk.Box(orientation: .vertical, spacing: 0) let ledger = ChildLedger(host: box) mountRegion(ledger.addRegion(), ctx: ctx) return box } }