portico/Sources/Portico/Core/ChildLedger.swift

111 lines
4.2 KiB
Swift

import Gtk
/// Tracks static children and independently mutable dynamic regions in one host.
///
/// A ledger converts a region-local position into the host's absolute position and
/// supplies the preceding sibling required by sibling-based widget APIs.
@_spi(Portico) @MainActor public final class ChildLedger {
private let host: any DynamicChildHost
fileprivate var regions: [[Gtk.Widget]] = []
private var staticTail: Int?
/// Creates a ledger for a freshly mounted dynamic child host.
@_spi(Portico) public init(host: any DynamicChildHost) {
self.host = host
}
/// Records a widget already appended directly to the host.
@_spi(Portico) public func appendStatic(_ child: Gtk.Widget) {
if staticTail == nil {
regions.append([])
staticTail = regions.count - 1
}
regions[staticTail!].append(child)
}
/// Opens an independently mutable region at the current tail.
@_spi(Portico) public func addRegion() -> ChildRegion {
regions.append([])
staticTail = nil
return ChildRegion(ledger: self, index: regions.count - 1)
}
fileprivate func base(of regionIndex: Int) -> Int {
regions[..<regionIndex].reduce(0) { $0 + $1.count }
}
fileprivate func widget(before absolute: Int) -> Gtk.Widget? {
guard absolute > 0 else { return nil }
var offset = absolute - 1
for region in regions {
if offset < region.count {
return region[offset]
}
offset -= region.count
}
return nil
}
fileprivate func insert(_ child: Gtk.Widget, at position: Int, in regionIndex: Int) {
regions[regionIndex].insert(child, at: position)
let absolute = base(of: regionIndex) + position
host.insertChild(child, at: absolute, after: widget(before: absolute))
}
fileprivate func remove(at position: Int, in regionIndex: Int) {
let child = regions[regionIndex].remove(at: position)
host.removeChild(child)
}
/// Moves a contiguous range to a region-local destination.
///
/// Widgets are removed from the host before any are inserted, so index-based
/// hosts (ListBox, FlowBox, Carousel) that use remove-then-insert see stable
/// absolute positions for each insertion. The ledger is updated to the final
/// order before insertion, so the sibling computed from `widget(before:)`
/// is already in its final host position for sibling-based hosts (Box, WrapBox).
fileprivate func move(from: Int, count: Int, to: Int, in regionIndex: Int) {
guard count != 0, from != to else { return }
let moved = Array(regions[regionIndex][from..<(from + count)])
for child in moved { host.removeChild(child) }
regions[regionIndex].removeSubrange(from..<(from + count))
regions[regionIndex].insert(contentsOf: moved, at: to)
let base = base(of: regionIndex)
for (offset, child) in moved.enumerated() {
let absolute = base + to + offset
host.insertChild(child, at: absolute, after: widget(before: absolute))
}
}
}
/// A mutable contiguous region owned by a ``ChildLedger``.
@_spi(Portico) @MainActor public struct ChildRegion {
private let ledger: ChildLedger
private let index: Int
/// Creates a region handle for the ledger's current region.
fileprivate init(ledger: ChildLedger, index: Int) {
self.ledger = ledger
self.index = index
}
/// The number of widgets currently held by this region.
@_spi(Portico) public var count: Int {
ledger.regions[index].count
}
/// Inserts a widget at a region-local position.
@_spi(Portico) public func insert(_ child: Gtk.Widget, at position: Int) {
ledger.insert(child, at: position, in: index)
}
/// Removes the widget at a region-local position.
@_spi(Portico) public func remove(at position: Int) {
ledger.remove(at: position, in: index)
}
/// Moves a contiguous range to a region-local destination.
@_spi(Portico) public func move(from: Int, count: Int, to: Int) {
ledger.move(from: from, count: count, to: to, in: index)
}
}