@_spi(SGTKInternal) import Gtk /// SPI protocol for views that can mount multiple widgets into one parent region. @_spi(Portico) @MainActor public protocol DynamicViewGroup { /// Mounts the group's widgets into `region` and registers reactive resources. func mountRegion(_ region: ChildRegion, ctx: MountContext) } /// A one-widget host used when a reactive conditional occupies a widget slot. /// /// Unlike a normal container, this host never inserts a wrapper widget. It keeps /// the currently selected branch widget and forwards replacement events to the /// property setter that owns the slot. @MainActor private final class SingleWidgetHost: DynamicChildHost { var current: Gtk.Widget? var onUpdate: ((Gtk.Widget?) -> Void)? func appendChild(_ child: Gtk.Widget) { insertChild(child, at: 0, after: nil) } func removeChild(_ child: Gtk.Widget) { guard current?.pointer == child.pointer else { return } current = nil onUpdate?(nil) } func insertChild(_ child: Gtk.Widget, at index: Int, after sibling: Gtk.Widget?) { precondition(current == nil, "a conditional branch in a single-widget slot produced multiple widgets") current = child onUpdate?(child) } func moveChild(_ child: Gtk.Widget, to index: Int, after sibling: Gtk.Widget?) { precondition(current?.pointer == child.pointer, "single-widget slot moved an unknown widget") } var conditionalStrategy: ConditionalStrategy { .insertion } } /// Owns the live mount for a required or optional single-widget slot. /// /// Static content is mounted directly. A conditional content node is reconciled /// against ``SingleWidgetHost`` so each selected branch is a raw widget and the /// owning generated setter receives branch replacements in place. @_spi(Portico) @MainActor public final class SingleChildSlot { /// The currently selected raw widget, or `nil` for an inactive optional branch. @_spi(Portico) public let widget: Gtk.Widget? private let host: SingleWidgetHost? /// Creates a live single-widget slot from a deferred view. /// /// - Parameters: /// - view: Produces exactly one view node when evaluated. /// - ctx: Mount context used for child resources and teardown. @_spi(Portico) public init( _ view: @escaping () -> AnyView, _ ctx: MountContext ) { let initial = view() switch initial.kind { case .conditional: let host = SingleWidgetHost() self.host = host let ledger = ChildLedger(host: host) let reconciler = ConditionalReconciler( views: { [view()] }, host: host, ledger: ledger, context: ctx, append: { _ in } ) reconciler.start() self.widget = host.current case .list: self.host = nil self.widget = initial.makeWidget(ctx) case .widget, .group: self.host = nil self.widget = initial.makeWidget(ctx) } } /// Connects the slot to its owning widget property. /// /// The callback is invoked only after the initial widget has been returned; /// later branch changes pass the new raw widget or `nil` for an empty branch. @_spi(Portico) public func attach(_ onUpdate: @escaping (Gtk.Widget?) -> Void) { host?.onUpdate = onUpdate } } /// Mounts an array of views into an ordered parent container. /// /// Static arrays retain the original one-pass mount path. Conditional and list /// nodes use a shared reconciler when the host supports indexed child control; /// unsupported hosts receive a private vertical box fallback. @_spi(Portico) @MainActor public func mountChildren( _ views: @escaping () -> [AnyView], into host: Gtk.Widget, _ ctx: MountContext, append: @escaping (Gtk.Widget) -> Void ) { let initial = views() guard initial.contains(where: { containsDynamicNode($0.kind) }) else { if let dynamicHost = host as? any DynamicChildHost { let ledger = ChildLedger(host: dynamicHost) for view in initial { if case .group(let group) = view.kind { group.mountRegion(ledger.addRegion(), ctx: ctx) } else { let widget = view.makeWidget(ctx) append(widget) ledger.appendStatic(widget) } } } else { for view in initial { append(view.makeWidget(ctx)) } } return } guard let dynamicHost = host as? any DynamicChildHost else { let box = Gtk.Box(orientation: .vertical, spacing: 0) append(box) mountChildren(views, into: box, ctx, append: { child in box.appendChild(child) }) return } let ledger = ChildLedger(host: dynamicHost) let reconciler = ConditionalReconciler( views: views, host: dynamicHost, ledger: ledger, context: ctx, append: append ) reconciler.start() } /// Mounts a required single-widget slot and returns its raw widget. @_spi(Portico) @MainActor public func mountSingleChild( _ view: @escaping () -> AnyView, _ ctx: MountContext ) -> Gtk.Widget { let slot = SingleChildSlot(view, ctx) guard let widget = slot.widget else { preconditionFailure("a required single-widget slot produced no widget") } return widget } /// Mounts a slot that accepts zero or one widget. @_spi(Portico) @MainActor public func mountSlotChild( _ views: @escaping () -> [AnyView], _ ctx: MountContext, onUpdate: ((Gtk.Widget?) -> Void)? = nil ) -> Gtk.Widget? { let initial = views() guard !initial.isEmpty else { return nil } precondition(initial.count == 1, "single widget slot produced more than one view") let slot = SingleChildSlot({ let current = views() precondition(current.count == 1, "single widget slot produced more than one view") return current[0] }, ctx) if let onUpdate { slot.attach(onUpdate) } return slot.widget } @MainActor private func containsDynamicNode(_ kind: ViewNodeKind) -> Bool { switch kind { case .conditional, .list: return true case .widget, .group: return false } } /// Provides a private-box mount for a conditional reached outside a container. @MainActor func mountConditionalFallback(_ node: ConditionalNode, ctx: MountContext) -> Gtk.Widget { let box = Gtk.Box(orientation: .vertical, spacing: 0) mountChildren({ [AnyView.conditional(node)] }, into: box, ctx, append: { child in box.appendChild(child) }) return box } /// Provides a private-box mount for a list reached outside a container. @MainActor func mountListFallback(_ rows: [[AnyView]], ctx: MountContext) -> Gtk.Widget { let box = Gtk.Box(orientation: .vertical, spacing: 0) mountChildren({ [AnyView.list(rows)] }, into: box, ctx, append: { child in box.appendChild(child) }) return box }