diff --git a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift index 89960f0..0fda16d 100644 --- a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift +++ b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift @@ -18,22 +18,19 @@ final class ApplicationRuntime { $0.disablesAnimation = true } run: { tx in - tx.scheduler.addCommitAction { [self, transaction = tx.transaction, rootView] ctx in - self.rootNode = - ctx.withMountContext(transaction: transaction) { - (mountCtx: consuming _MountContext) in - let node = _ConditionalNode( - isA: true, - context: _ViewContext(), - ctx: &mountCtx, - makeActive: { viewContext, mountCtx in - RootView._makeNode(rootView, context: viewContext, ctx: &mountCtx) - } - ) - - _ = mountCtx.mountInDOMNode(domRoot, observers: []) - return node + self.rootNode = scheduler.withMountContext(tx: &tx) { + (mountCtx: consuming _MountContext) in + let node = _ConditionalNode( + isA: true, + context: _ViewContext(), + ctx: &mountCtx, + makeActive: { viewContext, mountCtx in + RootView._makeNode(rootView, context: viewContext, ctx: &mountCtx) } + ) + + _ = mountCtx.mountInDOMNode(domRoot, isRoot: true) + return node } } } diff --git a/Sources/ElementaryUI/Reconciling/KeyedDiffEngine.swift b/Sources/ElementaryUI/Reconciling/KeyedDiffEngine.swift index 4ab3efa..b83bfdc 100644 --- a/Sources/ElementaryUI/Reconciling/KeyedDiffEngine.swift +++ b/Sources/ElementaryUI/Reconciling/KeyedDiffEngine.swift @@ -22,7 +22,7 @@ struct KeyedDiffEngine: ~Copyable { leavingSlots: inout UniqueArray, removedSlots: inout UniqueArray, keys: borrowing Span<_ViewKey>, - transaction: Transaction + makeNewSlot: (Int, _ViewKey) -> MountContainer.Slot ) -> Bool { let oldCount = activeSlots.count let newCount = keys.count @@ -72,25 +72,22 @@ struct KeyedDiffEngine: ~Copyable { outputSpan.append(self.takeActiveCell(at: source)) } else if source == KeyedDiffSource.new { outputSpan.append( - .pending( - key: newMiddleKeys[unchecked: middleOffset], - transaction: transaction, - newKeyIndex: prefixCount + middleOffset + makeNewSlot( + prefixCount + middleOffset, + newMiddleKeys[unchecked: middleOffset] ) ) } else { let leavingIndex = KeyedDiffSource.decodeRevive(source) if leavingIndex >= 0 && leavingIndex < self.leavingCells.count, - var revived = self.leavingCells[leavingIndex].take() + let revived = self.leavingCells[leavingIndex].take() { - revived.markReviving() outputSpan.append(revived) } else { outputSpan.append( - .pending( - key: newMiddleKeys[unchecked: middleOffset], - transaction: transaction, - newKeyIndex: prefixCount + middleOffset + makeNewSlot( + prefixCount + middleOffset, + newMiddleKeys[unchecked: middleOffset] ) ) } diff --git a/Sources/ElementaryUI/Reconciling/LayoutContainer.swift b/Sources/ElementaryUI/Reconciling/LayoutContainer.swift index 288b57d..d619f7c 100644 --- a/Sources/ElementaryUI/Reconciling/LayoutContainer.swift +++ b/Sources/ElementaryUI/Reconciling/LayoutContainer.swift @@ -18,7 +18,7 @@ final class LayoutContainer { self.layoutObservers = layoutObservers } - func mountInitial(_ context: inout _CommitContext) { + func commitInitialLayout(_ context: inout _CommitContext) { context.scheduler.scratch.withLayoutEntryScratchFrame { scratch in var ops = LayoutPass(layoutContainer: self, scratch: consume scratch) layoutNodes.collect(into: &ops, context: &context, op: .added) diff --git a/Sources/ElementaryUI/Reconciling/MountContainer.swift b/Sources/ElementaryUI/Reconciling/MountContainer.swift index e2ffc2f..786bd05 100644 --- a/Sources/ElementaryUI/Reconciling/MountContainer.swift +++ b/Sources/ElementaryUI/Reconciling/MountContainer.swift @@ -12,8 +12,6 @@ final class MountContainer { private var removedMiddleSlots: UniqueArray = .init() private var leavingRemovalScratch: UniqueArray = .init() - private var pendingMakeNode: ((Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable)? - private init(context: borrowing _ViewContext, slots: consuming UniqueArray) { self.viewContext = copy context self.activeSlots = slots @@ -31,7 +29,7 @@ final class MountContainer { let mountedSlot = ctx.withMountRootContext { rootCtx in Slot.mounted( key: key, - mounted: rootCtx.consumeAsMountedState( + mounted: rootCtx.makeMountedSlot( newKeyIndex: 0, viewContext: context, makeNode: { _, viewContext, mountCtx in @@ -58,7 +56,7 @@ final class MountContainer { let mountedSlot = ctx.withMountRootContext { rootCtx in Slot.mounted( key: keys[unchecked: index], - mounted: rootCtx.consumeAsMountedState( + mounted: rootCtx.makeMountedSlot( newKeyIndex: index, viewContext: context, makeNode: { index, viewContext, mountCtx in @@ -86,13 +84,9 @@ final class MountContainer { activeSlots[index].collectActive( into: &ops, context: &context, - viewContext: viewContext, - makeNode: pendingMakeNode, parentOp: op ) } - - pendingMakeNode = nil } func unmount(_ context: inout _CommitContext) { @@ -119,27 +113,30 @@ final class MountContainer { func patch( keys newKeys: borrowing Span<_ViewKey>, tx: inout _TransactionContext, - makeNode: @escaping (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, + makeNode: (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { - pendingMakeNode = makeNode - patchPrepared(keys: newKeys, tx: &tx, patchNode: patchNode) + patchPrepared(keys: newKeys, tx: &tx, makeNode: makeNode, patchNode: patchNode) } func patch( key newKey: _ViewKey, tx: inout _TransactionContext, - makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, + makeNode: (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, patchNode: (inout AnyReconcilable, inout _TransactionContext) -> Void ) { - pendingMakeNode = { _, viewContext, mountCtx in makeNode(viewContext, &mountCtx) } - - patchPrepared(keys: CollectionOfOne(newKey).span, tx: &tx) { _, node, tx in patchNode(&node, &tx) } + patchPrepared( + keys: CollectionOfOne(newKey).span, + tx: &tx, + makeNode: { _, viewContext, mountCtx in makeNode(viewContext, &mountCtx) }, + patchNode: { _, node, tx in patchNode(&node, &tx) } + ) } private func patchPrepared( keys: borrowing Span<_ViewKey>, tx: inout _TransactionContext, + makeNode: (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { prepareLaneCapacities(newCount: keys.count) @@ -152,14 +149,24 @@ final class MountContainer { leavingSlots: &leavingSlots, removedSlots: &removedMiddleSlots, keys: keys, - transaction: tx.transaction + makeNewSlot: { [viewContext] newKeyIndex, key in + var mounted = tx.scheduler.withMountContext(tx: &tx) { mountCtx in + mountCtx.makeMountedSlot( + newKeyIndex: newKeyIndex, + viewContext: viewContext, + makeNode: makeNode + ) + } + mounted.transitionCoordinator?.scheduleEnterIdentityIfNeeded(scheduler: tx.scheduler) + mounted.placement = .added + return .mounted(key: key, mounted: mounted) + } ) } // TODO: fix this to move-only while var slot = removedMiddleSlots.popLast() { switch slot.beginRemovalForDiff(tx: &tx, handle: containerHandle) { - case .none: break case .removed(let removed): removedNodes.append(removed) case .leaving(let leavingSlot): leavingSlots.append(leavingSlot) } @@ -209,15 +216,16 @@ final class MountContainer { extension MountContainer { struct Slot: ~Copyable { - struct Pending { - var transaction: Transaction - var newKeyIndex: Int - } - struct Mounted: ~Copyable { + enum Placement { + case unchanged + case added + case moved + } + var node: AnyReconcilable var layoutNodes: RigidArray - var didMove: Bool + var placement: Placement var transitionCoordinator: MountRootTransitionCoordinator? deinit { @@ -225,64 +233,42 @@ extension MountContainer { } } - enum SlotState: ~Copyable { - case pending(Pending) - case mounted(Mounted) - case reviving(Mounted) - case removed - - static func pending(transaction: Transaction, newKeyIndex: Int) -> Self { - .pending(.init(transaction: transaction, newKeyIndex: newKeyIndex)) - } - } - enum RemovalForDiff: ~Copyable { - case none case leaving(Slot) case removed(MountContainer.RemovedNode) } - let key: _ViewKey - var slotState: SlotState - - static func pending( - key: _ViewKey, - transaction: Transaction, - newKeyIndex: Int - ) -> Self { - .init( - key: key, - slotState: .pending(transaction: transaction, newKeyIndex: newKeyIndex) - ) + private enum Storage: ~Copyable { + case mounted(Mounted) + case movedOut } + let key: _ViewKey + private var storage: Storage + static func mounted( key: _ViewKey, mounted: consuming Mounted ) -> Self { - .init(key: key, slotState: .mounted(mounted)) + .init(key: key, storage: .mounted(mounted)) } @inline(__always) - private mutating func takeState() -> SlotState { - var state = SlotState.removed - swap(&state, &slotState) - return state - } - - @inline(__always) - private mutating func setPending(transaction: Transaction, newKeyIndex: Int) { - slotState = .pending(transaction: transaction, newKeyIndex: newKeyIndex) - } + private mutating func takeMounted() -> Mounted { + var storage = Storage.movedOut + swap(&storage, &self.storage) - @inline(__always) - private mutating func setMounted(_ mounted: consuming Mounted) { - slotState = .mounted(mounted) + switch consume storage { + case .mounted(let mounted): + return mounted + case .movedOut: + preconditionFailure("slot mounted state was already moved out") + } } @inline(__always) - private mutating func setRemoved() { - slotState = .removed + private mutating func putMounted(_ mounted: consuming Mounted) { + self.storage = .mounted(mounted) } @inline(__always) @@ -313,172 +299,99 @@ extension MountContainer { } } - mutating func markReviving() { - let state = takeState() - switch consume state { - case .mounted(let mounted): - slotState = .reviving(mounted) - default: - preconditionFailure("only mounted leaving slots can be marked for revival") - } - } - mutating func patchInActiveLane( newKeyIndex: Int, tx: inout _TransactionContext, containerHandle: LayoutContainer.Handle?, patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { - let state = takeState() - - switch consume state { - case .pending: - setPending(transaction: tx.transaction, newKeyIndex: newKeyIndex) - case .mounted(var mounted): - patchNode(newKeyIndex, &mounted.node, &tx) - setMounted(mounted) - case .reviving(var mounted): + var mounted = takeMounted() + + if mounted.transitionCoordinator?.isRemovalInFlight == true { mounted.transitionCoordinator?.cancelRemoval(tx: &tx) - mounted.didMove = true Self.reportReenteringElements(of: mounted, handle: containerHandle, tx: &tx) - patchNode(newKeyIndex, &mounted.node, &tx) - setMounted(mounted) - case .removed: - preconditionFailure("active lane contains removed slot") + mounted.placement = .moved } + + patchNode(newKeyIndex, &mounted.node, &tx) + putMounted(consume mounted) } mutating func markMovedInActiveLane() { - let state = takeState() + var mounted = takeMounted() - switch consume state { - case .pending(let pending): - slotState = .pending(pending) - case .mounted(let mounted): - var mounted = mounted - mounted.didMove = true - setMounted(mounted) - case .reviving: - preconditionFailure("reviving slot should not appear in activeCells") - case .removed: - preconditionFailure("active lane contains removed slot") + if mounted.placement == .unchanged { + mounted.placement = .moved } + putMounted(consume mounted) } mutating func beginRemovalForDiff( tx: inout _TransactionContext, handle: LayoutContainer.Handle? ) -> RemovalForDiff { - let state = takeState() - - switch consume state { - case .pending: - setRemoved() - return .none + let mounted = takeMounted() - case .mounted(let mounted): - let shouldDeferRemoval = mounted.transitionCoordinator?.beginRemoval(tx: &tx, handle: handle) ?? false - - if shouldDeferRemoval { - Self.reportLeavingElements(of: mounted, handle: handle, tx: &tx) - return .leaving(.mounted(key: key, mounted: mounted)) - } + if mounted.placement == .added { + return .removed(.init(mounted: mounted, shouldCollectLayout: false)) + } - setRemoved() - return .removed(.init(mounted: mounted)) + let shouldDeferRemoval = mounted.transitionCoordinator?.beginRemoval(tx: &tx, handle: handle) ?? false - case .reviving: - preconditionFailure("reviving slot should not appear in removed slots") - case .removed: - preconditionFailure("active lane contains removed slot") + if shouldDeferRemoval { + Self.reportLeavingElements(of: mounted, handle: handle, tx: &tx) + return .leaving(.mounted(key: key, mounted: mounted)) } + + return .removed(.init(mounted: mounted)) } mutating func consumeRemovedIfReadyFromLeaving() -> MountContainer.RemovedNode? { - let state = takeState() - - switch consume state { - case .mounted(let mounted): - if mounted.transitionCoordinator?.consumeDeferredRemovalReadySignal() == true { - setRemoved() - return .init(mounted: mounted) - } - - setMounted(mounted) - return nil + let mounted = takeMounted() - case .reviving: - preconditionFailure("reviving slot should not appear in leaving lane") - case .pending: - preconditionFailure("leaving lane contains non-mounted slot") - case .removed: - preconditionFailure("leaving lane contains non-mounted slot") + if mounted.transitionCoordinator?.consumeDeferredRemovalReadySignal() == true { + return .init(mounted: mounted) } + + putMounted(consume mounted) + return nil } mutating func collectActive( into ops: inout LayoutPass, context: inout _CommitContext, - viewContext: borrowing _ViewContext, - makeNode: ((Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable)?, parentOp: LayoutPass.Entry.LayoutOp ) { - let state = takeState() - - switch consume state { - case .pending(let pending): - guard let makeNode else { - preconditionFailure("pending slot requires a makeNode callback") - } - let mounted = context.withMountContext(transaction: pending.transaction) { mountCtx in - mountCtx.consumeAsMountedState( - newKeyIndex: pending.newKeyIndex, - viewContext: viewContext, - makeNode: makeNode - ) - } - - mounted.transitionCoordinator?.scheduleEnterIdentityIfNeeded(scheduler: context.scheduler) - mounted.layoutNodes.collect(into: &ops, context: &context, op: .added) - setMounted(mounted) - - case .mounted(let mounted): - var mounted = mounted - let childOp: LayoutPass.Entry.LayoutOp = mounted.didMove ? .moved : parentOp - mounted.layoutNodes.collect(into: &ops, context: &context, op: childOp) - mounted.didMove = false - setMounted(mounted) - - case .reviving: - preconditionFailure("reviving slot should have been resolved in patchInActiveLane") - case .removed: - preconditionFailure("active lane contains removed slot") + var mounted = takeMounted() + + let childOp: LayoutPass.Entry.LayoutOp + switch mounted.placement { + case .unchanged: + childOp = parentOp + case .added: + childOp = .added + case .moved: + childOp = .moved } + mounted.layoutNodes.collect(into: &ops, context: &context, op: childOp) + mounted.placement = .unchanged + putMounted(consume mounted) } mutating func unmount(_ context: inout _CommitContext) { - let state = takeState() - - switch consume state { - case .pending: - setRemoved() - case .mounted(let mounted): - mounted.node.unmount(&context) - setRemoved() - case .reviving(let mounted): - mounted.node.unmount(&context) - setRemoved() - case .removed: - setRemoved() - } + let mounted = takeMounted() + mounted.node.unmount(&context) } } struct RemovedNode: ~Copyable { var mounted: Slot.Mounted + var shouldCollectLayout: Bool = true mutating func collectRemoved(into ops: inout LayoutPass, context: inout _CommitContext) { - mounted.layoutNodes.collect(into: &ops, context: &context, op: .removed) + if shouldCollectLayout { + mounted.layoutNodes.collect(into: &ops, context: &context, op: .removed) + } mounted.node.unmount(&context) } diff --git a/Sources/ElementaryUI/Reconciling/Scheduler.swift b/Sources/ElementaryUI/Reconciling/Scheduler.swift index 800f6f9..8a82d46 100644 --- a/Sources/ElementaryUI/Reconciling/Scheduler.swift +++ b/Sources/ElementaryUI/Reconciling/Scheduler.swift @@ -126,6 +126,24 @@ final class Scheduler { context = ambientContext.take()! } + func withMountContext( + tx: inout _TransactionContext, + _ body: (consuming _MountContext) -> R + ) -> R { + self.scratch.withLayoutNodeScratchFrame { scratch in + body( + _MountContext( + nodeStack: scratch, + dom: dom, + scheduler: self, + currentFrameTime: tx.currentFrameTime, + transaction: tx.transaction, + isRoot: true + ) + ) + } + } + // MARK: - Scheduling private func ensureUpdateCycleScheduled() { diff --git a/Sources/ElementaryUI/Reconciling/_MountContext.swift b/Sources/ElementaryUI/Reconciling/_MountContext.swift index 92a5ee0..b611af7 100644 --- a/Sources/ElementaryUI/Reconciling/_MountContext.swift +++ b/Sources/ElementaryUI/Reconciling/_MountContext.swift @@ -15,7 +15,7 @@ public struct _MountContext: ~Copyable, ~Escapable { let transaction: Transaction @_lifetime(copy nodeStack) - fileprivate init( + init( nodeStack: consuming ScratchStack, dom: any DOM.Interactor, scheduler: Scheduler, @@ -99,21 +99,7 @@ public struct _MountContext: ~Copyable, ~Escapable { return body(&commitContext) } - consuming func consumeAsLayoutContainer( - domNode: DOM.Node, - observers: [any DOMLayoutObserver] - ) -> LayoutContainer { - let scheduler = self.scheduler - let layoutNodes = takeMaterializedLayoutNodes() - return LayoutContainer( - domNode: domNode, - scheduler: scheduler, - layoutNodes: consume layoutNodes, - layoutObservers: observers - ) - } - - consuming func consumeAsMountedState( + consuming func makeMountedSlot( newKeyIndex: Int, viewContext: borrowing _ViewContext, makeNode: (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable @@ -124,12 +110,15 @@ public struct _MountContext: ~Copyable, ~Escapable { return MountContainer.Slot.Mounted( node: node, layoutNodes: takeMaterializedLayoutNodes(), - didMove: false, + placement: .unchanged, transitionCoordinator: transitionCoordinator ) } - consuming func mountInDOMNode(_ domNode: DOM.Node, observers: [any DOMLayoutObserver]) -> LayoutContainer? { + consuming func mountInDOMNode(_ domNode: DOM.Node, observers: [any DOMLayoutObserver] = [], isRoot: Bool = false) -> LayoutContainer? { + // only allow root node to be mounted directly, otherwise we need to always do this in a stacked context + assert(!self.isRoot || isRoot, "only root node can be mounted directly") + if isStatic { let dom = dom nodeStack.consume { span in @@ -143,13 +132,20 @@ public struct _MountContext: ~Copyable, ~Escapable { let dom = dom let scheduler = scheduler let currentFrameTime = currentFrameTime - let container = consumeAsLayoutContainer(domNode: domNode, observers: observers) + let container = LayoutContainer( + domNode: domNode, + scheduler: scheduler, + layoutNodes: takeMaterializedLayoutNodes(), + layoutObservers: observers + ) + var commit = _CommitContext( dom: dom, scheduler: scheduler, currentFrameTime: currentFrameTime ) - container.mountInitial(&commit) + + container.commitInitialLayout(&commit) return container } @@ -175,23 +171,3 @@ private extension LayoutNode { } } } - -extension _CommitContext { - func withMountContext( - transaction: Transaction, - _ body: (consuming _MountContext) -> R - ) -> R { - scheduler.scratch.withLayoutNodeScratchFrame { scratch in - body( - _MountContext( - nodeStack: scratch, - dom: dom, - scheduler: scheduler, - currentFrameTime: currentFrameTime, - transaction: transaction, - isRoot: true - ) - ) - } - } -} diff --git a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift index 6aa9f6c..fb09b69 100644 --- a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift +++ b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift @@ -22,7 +22,7 @@ public struct _ConditionalNode: ~Copyable, _Reconcilable { mutating func patchWithA( tx: inout _TransactionContext, - makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> NodeA, + makeNode: (borrowing _ViewContext, inout _MountContext) -> NodeA, updateNode: (inout NodeA, inout _TransactionContext) -> Void ) { patchBranch( @@ -35,7 +35,7 @@ public struct _ConditionalNode: ~Copyable, _Reconcilable { mutating func patchWithB( tx: inout _TransactionContext, - makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> NodeB, + makeNode: (borrowing _ViewContext, inout _MountContext) -> NodeB, updateNode: (inout NodeB, inout _TransactionContext) -> Void ) { patchBranch( @@ -55,7 +55,7 @@ private extension _ConditionalNode { mutating func patchBranch( key: _ViewKey, tx: inout _TransactionContext, - makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> Node, + makeNode: (borrowing _ViewContext, inout _MountContext) -> Node, updateNode: (inout Node, inout _TransactionContext) -> Void ) { container.patch( diff --git a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift index 561a25b..f7dedad 100644 --- a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift +++ b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift @@ -34,7 +34,7 @@ public struct _KeyedNode: ~Copyable, _Reconcilable { mutating func patch( key: _ViewKey, context: inout _TransactionContext, - makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, + makeNode: (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, patchNode: (inout AnyReconcilable, inout _TransactionContext) -> Void ) { container.patch( @@ -48,7 +48,7 @@ public struct _KeyedNode: ~Copyable, _Reconcilable { mutating func patch( _ newKeys: borrowing Span<_ViewKey>, context: inout _TransactionContext, - makeNode: @escaping (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, + makeNode: (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { container.patch( diff --git a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift index a91a648..2b14a1c 100644 --- a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift +++ b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift @@ -68,6 +68,7 @@ where ChildNode == Value.Body._MountedNode { public consuming func unmount(_ context: inout _CommitContext) { switch storage { case .inline(var child): + __noOpModifyForStupidWarning(&child) child.unmount(&context) case .box(let s): s.trackingSession.take()?.cancel() @@ -194,3 +195,8 @@ where Child == Value.Body, ChildNode == Child._MountedNode { child.take()?.unmount(&context) } } + +@_transparent +private func __noOpModifyForStupidWarning(_ value: inout R) { + // do nothing +} diff --git a/Tests/ElementaryUITests/Reconciler/DOMPatchingTest.swift b/Tests/ElementaryUITests/Reconciler/DOMPatchingTest.swift index ee04314..8fa0c21 100644 --- a/Tests/ElementaryUITests/Reconciler/DOMPatchingTest.swift +++ b/Tests/ElementaryUITests/Reconciler/DOMPatchingTest.swift @@ -405,6 +405,39 @@ struct DOMPatchingTests { ) } + @Test + func patchesNestedConditionals() { + let state = StringListState([]) + let ops = patchOps { + ForEach(state.items, key: \.self) { item in + p { + if true { + item + } + } + } + } toggle: { + state.items = ["B", "C", "D"] + } + + #expect( + ops == [ + .createElement("p"), + .createText("B"), + .addChild(parent: "

", child: "B"), + .createElement("p"), + .createText("C"), + .addChild(parent: "

", child: "C"), + .createElement("p"), + .createText("D"), + .addChild(parent: "

", child: "D"), + .addChild(parent: "<>", child: "

"), + .addChild(parent: "<>", child: "

"), + .addChild(parent: "<>", child: "

"), + ] + ) + } + @Test func countsUp() { let state = CounterState()