From 9fef1210f8872ce8267397760cf61a6aa59515a5 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:06:42 +0200 Subject: [PATCH 1/3] WIP: non-copyable nodes --- Package.swift | 3 +- .../Data/Lifecycle/View+LifecycleEvents.swift | 2 +- .../Data/Lifecycle/_StatefulNode.swift | 10 +- .../ElementaryUI/HTMLViews/_ElementNode.swift | 2 +- .../Reconciling/ApplicationRuntime.swift | 2 +- .../Reconciling/MountContainer.swift | 4 +- .../Reconciling/_Reconcilable.swift | 16 +- .../StructureViews/Tuples+Mountable.swift | 42 ++--- .../StructureViews/_ConditionalNode.swift | 10 +- .../StructureViews/_EmptyNode.swift | 2 +- .../StructureViews/_KeyedNode.swift | 6 +- .../StructureViews/_TupleNode.swift | 145 +++++++++++------- .../Views/Function/_FunctionNode.swift | 6 +- .../Views/Function/_FunctionView.swift | 10 +- .../ElementaryUI/Views/View+Mountable.swift | 2 +- Sources/ElementaryUIMacros/ViewMacro.swift | 2 +- 16 files changed, 152 insertions(+), 112 deletions(-) diff --git a/Package.swift b/Package.swift index 57fc4865..c5bce828 100644 --- a/Package.swift +++ b/Package.swift @@ -32,10 +32,11 @@ let package = Package( ], swiftSettings: [ .swiftLanguageMode(.v5), - .enableExperimentalFeature("Lifetimes"), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ImplicitOpenExistentials"), + .enableExperimentalFeature("Lifetimes"), + .enableExperimentalFeature("SuppressedAssociatedTypes"), ] ), .target( diff --git a/Sources/ElementaryUI/Data/Lifecycle/View+LifecycleEvents.swift b/Sources/ElementaryUI/Data/Lifecycle/View+LifecycleEvents.swift index e5ef0855..d182cf60 100644 --- a/Sources/ElementaryUI/Data/Lifecycle/View+LifecycleEvents.swift +++ b/Sources/ElementaryUI/Data/Lifecycle/View+LifecycleEvents.swift @@ -95,7 +95,7 @@ enum LifecycleHook { case onAppearReturningCancelFunction(() -> () -> Void) } -struct _LifecycleEventView: View { +struct _LifecycleEventView: View, _Mountable { typealias Tag = Wrapped.Tag typealias _MountedNode = _StatefulNode diff --git a/Sources/ElementaryUI/Data/Lifecycle/_StatefulNode.swift b/Sources/ElementaryUI/Data/Lifecycle/_StatefulNode.swift index 1ae47d35..0d425002 100644 --- a/Sources/ElementaryUI/Data/Lifecycle/_StatefulNode.swift +++ b/Sources/ElementaryUI/Data/Lifecycle/_StatefulNode.swift @@ -1,25 +1,23 @@ -public struct _StatefulNode { +public struct _StatefulNode: ~Copyable, _Reconcilable { var state: State var child: Child private var onUnmount: ((inout _CommitContext) -> Void)? - init(state: State, child: Child) { + init(state: consuming State, child: consuming Child) { self.state = state self.child = child } - private init(state: State, child: Child, onUnmount: ((inout _CommitContext) -> Void)? = nil) { + private init(state: consuming State, child: consuming Child, onUnmount: ((inout _CommitContext) -> Void)? = nil) { self.state = state self.child = child self.onUnmount = onUnmount } - init(state: State, child: Child) where State: Unmountable { + init(state: consuming State, child: consuming Child) where State: Unmountable { self.init(state: state, child: child, onUnmount: state.unmount(_:)) } -} -extension _StatefulNode: _Reconcilable { public consuming func unmount(_ context: inout _CommitContext) { child.unmount(&context) onUnmount?(&context) diff --git a/Sources/ElementaryUI/HTMLViews/_ElementNode.swift b/Sources/ElementaryUI/HTMLViews/_ElementNode.swift index a5fd71fd..8ade09fd 100644 --- a/Sources/ElementaryUI/HTMLViews/_ElementNode.swift +++ b/Sources/ElementaryUI/HTMLViews/_ElementNode.swift @@ -3,7 +3,7 @@ enum _ElementAttributes { case modifier(_AttributeModifier) } -public struct _ElementNode: _Reconcilable { +public struct _ElementNode: ~Copyable, _Reconcilable { private var child: Child private var attributes: _ElementAttributes private var mountedModifiers: [AnyUnmountable] = [] diff --git a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift index 566b9012..89960f09 100644 --- a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift +++ b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift @@ -49,7 +49,7 @@ final class ApplicationRuntime { rootNode.patchWithB(tx: &tx, makeNode: { _, _ in _EmptyNode() }, updateNode: { _, _ in }) // Break the root container/layout cycle after patch-driven removals are committed. - tx.scheduler.addCommitAction { ctx in rootNode.unmount(&ctx) } + tx.scheduler.addCommitAction { ctx in rootNode.container.unmount(&ctx) } } } } diff --git a/Sources/ElementaryUI/Reconciling/MountContainer.swift b/Sources/ElementaryUI/Reconciling/MountContainer.swift index 436cf7f2..58cca37f 100644 --- a/Sources/ElementaryUI/Reconciling/MountContainer.swift +++ b/Sources/ElementaryUI/Reconciling/MountContainer.swift @@ -19,7 +19,7 @@ final class MountContainer { self.activeSlots = slots } - convenience init( + convenience init( mountedKey key: _ViewKey, context: borrowing _ViewContext, ctx: inout _MountContext, @@ -45,7 +45,7 @@ final class MountContainer { ) } - convenience init( + convenience init( mountedKeyStorage keys: borrowing Span<_ViewKey>, context: borrowing _ViewContext, ctx: inout _MountContext, diff --git a/Sources/ElementaryUI/Reconciling/_Reconcilable.swift b/Sources/ElementaryUI/Reconciling/_Reconcilable.swift index 7f0bbe5a..03f8f42a 100644 --- a/Sources/ElementaryUI/Reconciling/_Reconcilable.swift +++ b/Sources/ElementaryUI/Reconciling/_Reconcilable.swift @@ -1,6 +1,6 @@ // TODO: either get rid of this protocol entirely, or turn it into a dedicated // mount lifecycle owner type. -public protocol _Reconcilable { +public protocol _Reconcilable: ~Copyable { consuming func unmount(_ context: inout _CommitContext) } @@ -9,19 +9,19 @@ struct AnyReconcilable { func unmount(_ context: inout _CommitContext) {} } - final class _TypedBox: _Box { - var node: R + final class _TypedBox: _Box { + var node: R? - init(_ node: consuming R) { self.node = node } + init(_ node: consuming R) { self.node = .some(node) } override func unmount(_ context: inout _CommitContext) { - node.unmount(&context) + node.take()?.unmount(&context) } } private var box: _Box - init(_ node: R) { + init(_ node: consuming R) { self.box = _TypedBox(node) } @@ -30,8 +30,8 @@ struct AnyReconcilable { } // TODO: make this mutating to prepare for ~Copyable all the way - func modify(as type: R.Type = R.self, _ body: (inout R) -> Void) { + func modify(as type: R.Type = R.self, _ body: (inout R) -> Void) { let box = unsafeDowncast(self.box, to: _TypedBox.self) - body(&box.node) + body(&box.node!) } } diff --git a/Sources/ElementaryUI/StructureViews/Tuples+Mountable.swift b/Sources/ElementaryUI/StructureViews/Tuples+Mountable.swift index 3362d5fa..552b3560 100644 --- a/Sources/ElementaryUI/StructureViews/Tuples+Mountable.swift +++ b/Sources/ElementaryUI/StructureViews/Tuples+Mountable.swift @@ -18,8 +18,8 @@ extension _HTMLTuple2: _Mountable where V0: _Mountable, V1: _Mountable { node: inout _MountedNode, tx: inout _TransactionContext ) { - V0._patchNode(view.v0, node: &node.value.0, tx: &tx) - V1._patchNode(view.v1, node: &node.value.1, tx: &tx) + V0._patchNode(view.v0, node: &node.n0, tx: &tx) + V1._patchNode(view.v1, node: &node.n1, tx: &tx) } } @@ -44,9 +44,9 @@ extension _HTMLTuple3: _Mountable where V0: _Mountable, V1: _Mountable, V2: _Mou node: inout _MountedNode, tx: inout _TransactionContext ) { - V0._patchNode(view.v0, node: &node.value.0, tx: &tx) - V1._patchNode(view.v1, node: &node.value.1, tx: &tx) - V2._patchNode(view.v2, node: &node.value.2, tx: &tx) + V0._patchNode(view.v0, node: &node.n0, tx: &tx) + V1._patchNode(view.v1, node: &node.n1, tx: &tx) + V2._patchNode(view.v2, node: &node.n2, tx: &tx) } } @@ -72,10 +72,10 @@ extension _HTMLTuple4: _Mountable where V0: _Mountable, V1: _Mountable, V2: _Mou node: inout _MountedNode, tx: inout _TransactionContext ) { - V0._patchNode(view.v0, node: &node.value.0, tx: &tx) - V1._patchNode(view.v1, node: &node.value.1, tx: &tx) - V2._patchNode(view.v2, node: &node.value.2, tx: &tx) - V3._patchNode(view.v3, node: &node.value.3, tx: &tx) + V0._patchNode(view.v0, node: &node.n0, tx: &tx) + V1._patchNode(view.v1, node: &node.n1, tx: &tx) + V2._patchNode(view.v2, node: &node.n2, tx: &tx) + V3._patchNode(view.v3, node: &node.n3, tx: &tx) } } @@ -102,11 +102,11 @@ extension _HTMLTuple5: _Mountable where V0: _Mountable, V1: _Mountable, V2: _Mou node: inout _MountedNode, tx: inout _TransactionContext ) { - V0._patchNode(view.v0, node: &node.value.0, tx: &tx) - V1._patchNode(view.v1, node: &node.value.1, tx: &tx) - V2._patchNode(view.v2, node: &node.value.2, tx: &tx) - V3._patchNode(view.v3, node: &node.value.3, tx: &tx) - V4._patchNode(view.v4, node: &node.value.4, tx: &tx) + V0._patchNode(view.v0, node: &node.n0, tx: &tx) + V1._patchNode(view.v1, node: &node.n1, tx: &tx) + V2._patchNode(view.v2, node: &node.n2, tx: &tx) + V3._patchNode(view.v3, node: &node.n3, tx: &tx) + V4._patchNode(view.v4, node: &node.n4, tx: &tx) } } @@ -136,16 +136,16 @@ extension _HTMLTuple6: _Mountable where V0: _Mountable, V1: _Mountable, V2: _Mou node: inout _MountedNode, tx: inout _TransactionContext ) { - V0._patchNode(view.v0, node: &node.value.0, tx: &tx) - V1._patchNode(view.v1, node: &node.value.1, tx: &tx) - V2._patchNode(view.v2, node: &node.value.2, tx: &tx) - V3._patchNode(view.v3, node: &node.value.3, tx: &tx) - V4._patchNode(view.v4, node: &node.value.4, tx: &tx) - V5._patchNode(view.v5, node: &node.value.5, tx: &tx) + V0._patchNode(view.v0, node: &node.n0, tx: &tx) + V1._patchNode(view.v1, node: &node.n1, tx: &tx) + V2._patchNode(view.v2, node: &node.n2, tx: &tx) + V3._patchNode(view.v3, node: &node.n3, tx: &tx) + V4._patchNode(view.v4, node: &node.n4, tx: &tx) + V5._patchNode(view.v5, node: &node.n5, tx: &tx) } } -#if !hasFeature(Embedded) +#if false // non-copyable + tuples no bueno for now // Generic variadic tuple support using parameter packs extension _HTMLTuple: View where repeat each Child: View {} extension _HTMLTuple: _Mountable where repeat each Child: _Mountable { diff --git a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift index dd3f48c3..6aa9f6c1 100644 --- a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift +++ b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift @@ -1,10 +1,10 @@ private let keyA = _ViewKey(0) private let keyB = _ViewKey(1) -public struct _ConditionalNode: _Reconcilable { +public struct _ConditionalNode: ~Copyable, _Reconcilable { let container: MountContainer - init( + init( isA: Bool, context: borrowing _ViewContext, ctx: inout _MountContext, @@ -20,7 +20,7 @@ public struct _ConditionalNode: _Reconcilable { ctx.appendContainer(container) } - mutating func patchWithA( + mutating func patchWithA( tx: inout _TransactionContext, makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> NodeA, updateNode: (inout NodeA, inout _TransactionContext) -> Void @@ -33,7 +33,7 @@ public struct _ConditionalNode: _Reconcilable { ) } - mutating func patchWithB( + mutating func patchWithB( tx: inout _TransactionContext, makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> NodeB, updateNode: (inout NodeB, inout _TransactionContext) -> Void @@ -52,7 +52,7 @@ public struct _ConditionalNode: _Reconcilable { } private extension _ConditionalNode { - mutating func patchBranch( + mutating func patchBranch( key: _ViewKey, tx: inout _TransactionContext, makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> Node, diff --git a/Sources/ElementaryUI/StructureViews/_EmptyNode.swift b/Sources/ElementaryUI/StructureViews/_EmptyNode.swift index a7a4e3f5..7430ad3d 100644 --- a/Sources/ElementaryUI/StructureViews/_EmptyNode.swift +++ b/Sources/ElementaryUI/StructureViews/_EmptyNode.swift @@ -1,4 +1,4 @@ -public struct _EmptyNode: _Reconcilable { +public struct _EmptyNode: _Reconcilable & ~Copyable { public consuming func unmount(_ context: inout _CommitContext) { } } diff --git a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift index 780812bc..6bb1a667 100644 --- a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift +++ b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift @@ -1,7 +1,7 @@ -public struct _KeyedNode: _Reconcilable { +public struct _KeyedNode: ~Copyable, _Reconcilable { let container: MountContainer - init( + init( keys: borrowing Span<_ViewKey>, context: borrowing _ViewContext, ctx: inout _MountContext, @@ -16,7 +16,7 @@ public struct _KeyedNode: _Reconcilable { ctx.appendContainer(container) } - init( + init( key: _ViewKey, context: borrowing _ViewContext, ctx: inout _MountContext, diff --git a/Sources/ElementaryUI/StructureViews/_TupleNode.swift b/Sources/ElementaryUI/StructureViews/_TupleNode.swift index bd69200b..1c756c00 100644 --- a/Sources/ElementaryUI/StructureViews/_TupleNode.swift +++ b/Sources/ElementaryUI/StructureViews/_TupleNode.swift @@ -13,88 +13,129 @@ public struct _TupleNode: _Reconcilable { } } -public struct _TupleNode2: _Reconcilable { - var value: (N0, N1) +public struct _TupleNode2: ~Copyable, _Reconcilable { + var n0: N0 + var n1: N1 - init(_ n0: N0, _ n1: N1) { - self.value = (n0, n1) + init(_ n0: consuming N0, _ n1: consuming N1) { + self.n0 = n0 + self.n1 = n1 } public consuming func unmount(_ context: inout _CommitContext) { - value.0.unmount(&context) - value.1.unmount(&context) + n0.unmount(&context) + n1.unmount(&context) } } -public struct _TupleNode3: _Reconcilable { - var value: (N0, N1, N2) - - init(_ n0: N0, _ n1: N1, _ n2: N2) { - self.value = (n0, n1, n2) +public struct _TupleNode3: ~Copyable, + _Reconcilable +{ + var n0: N0 + var n1: N1 + var n2: N2 + + init(_ n0: consuming N0, _ n1: consuming N1, _ n2: consuming N2) { + self.n0 = n0 + self.n1 = n1 + self.n2 = n2 } public consuming func unmount(_ context: inout _CommitContext) { - value.0.unmount(&context) - value.1.unmount(&context) - value.2.unmount(&context) + n0.unmount(&context) + n1.unmount(&context) + n2.unmount(&context) } } -public struct _TupleNode4: _Reconcilable { - var value: (N0, N1, N2, N3) - - init(_ n0: N0, _ n1: N1, _ n2: N2, _ n3: N3) { - self.value = (n0, n1, n2, n3) +public struct _TupleNode4< + N0: _Reconcilable & ~Copyable, + N1: _Reconcilable & ~Copyable, + N2: _Reconcilable & ~Copyable, + N3: _Reconcilable & ~Copyable +>: ~Copyable, _Reconcilable { + var n0: N0 + var n1: N1 + var n2: N2 + var n3: N3 + + init(_ n0: consuming N0, _ n1: consuming N1, _ n2: consuming N2, _ n3: consuming N3) { + self.n0 = n0 + self.n1 = n1 + self.n2 = n2 + self.n3 = n3 } public consuming func unmount(_ context: inout _CommitContext) { - value.0.unmount(&context) - value.1.unmount(&context) - value.2.unmount(&context) - value.3.unmount(&context) + n0.unmount(&context) + n1.unmount(&context) + n2.unmount(&context) + n3.unmount(&context) } } -public struct _TupleNode5: - _Reconcilable -{ - var value: (N0, N1, N2, N3, N4) - - init(_ n0: N0, _ n1: N1, _ n2: N2, _ n3: N3, _ n4: N4) { - self.value = (n0, n1, n2, n3, n4) +public struct _TupleNode5< + N0: _Reconcilable & ~Copyable, + N1: _Reconcilable & ~Copyable, + N2: _Reconcilable & ~Copyable, + N3: _Reconcilable & ~Copyable, + N4: _Reconcilable & ~Copyable +>: ~Copyable, _Reconcilable { + var n0: N0 + var n1: N1 + var n2: N2 + var n3: N3 + var n4: N4 + + init(_ n0: consuming N0, _ n1: consuming N1, _ n2: consuming N2, _ n3: consuming N3, _ n4: consuming N4) { + self.n0 = n0 + self.n1 = n1 + self.n2 = n2 + self.n3 = n3 + self.n4 = n4 } public consuming func unmount(_ context: inout _CommitContext) { - value.0.unmount(&context) - value.1.unmount(&context) - value.2.unmount(&context) - value.3.unmount(&context) - value.4.unmount(&context) + n0.unmount(&context) + n1.unmount(&context) + n2.unmount(&context) + n3.unmount(&context) + n4.unmount(&context) } } public struct _TupleNode6< - N0: _Reconcilable, - N1: _Reconcilable, - N2: _Reconcilable, - N3: _Reconcilable, - N4: _Reconcilable, - N5: _Reconcilable + N0: _Reconcilable & ~Copyable, + N1: _Reconcilable & ~Copyable, + N2: _Reconcilable & ~Copyable, + N3: _Reconcilable & ~Copyable, + N4: _Reconcilable & ~Copyable, + N5: _Reconcilable & ~Copyable >: - _Reconcilable + ~Copyable, _Reconcilable { - var value: (N0, N1, N2, N3, N4, N5) - - init(_ n0: N0, _ n1: N1, _ n2: N2, _ n3: N3, _ n4: N4, _ n5: N5) { - self.value = (n0, n1, n2, n3, n4, n5) + var n0: N0 + var n1: N1 + var n2: N2 + var n3: N3 + var n4: N4 + var n5: N5 + + init(_ n0: consuming N0, _ n1: consuming N1, _ n2: consuming N2, _ n3: consuming N3, _ n4: consuming N4, _ n5: consuming N5) { + self.n0 = n0 + self.n1 = n1 + self.n2 = n2 + self.n3 = n3 + self.n4 = n4 + self.n5 = n5 } public consuming func unmount(_ context: inout _CommitContext) { - value.0.unmount(&context) - value.1.unmount(&context) - value.2.unmount(&context) - value.3.unmount(&context) - value.4.unmount(&context) - value.5.unmount(&context) + n0.unmount(&context) + n1.unmount(&context) + n2.unmount(&context) + n3.unmount(&context) + n4.unmount(&context) + n5.unmount(&context) } } diff --git a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift index ed0485b9..12f3ec3b 100644 --- a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift +++ b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift @@ -4,8 +4,8 @@ import Reactivity // NOTE: ChildNode must be specified as extra argument to avoid a compiler error in embedded // FIXME: embedded - try with embedded main-snapshot build, revert extra argument if it works -public final class _FunctionNode -where Value: __FunctionView, ChildNode: _Reconcilable, ChildNode == Value.Body._MountedNode { +public final class _FunctionNode +where Value: __FunctionView { private var state: Value.__ViewState? private var value: Value? private var context: _ViewContext? @@ -134,7 +134,7 @@ extension _FunctionNode: _Reconcilable { } extension AnyFunctionNode { - init(_ function: _FunctionNode) { + init(_ function: _FunctionNode) { self.identifier = ObjectIdentifier(function) self.depthInTree = function.depthInTree self.runUpdate = function.runFunction diff --git a/Sources/ElementaryUI/Views/Function/_FunctionView.swift b/Sources/ElementaryUI/Views/Function/_FunctionView.swift index 51512c74..ae36f9fe 100644 --- a/Sources/ElementaryUI/Views/Function/_FunctionView.swift +++ b/Sources/ElementaryUI/Views/Function/_FunctionView.swift @@ -1,4 +1,4 @@ -public protocol __FunctionView: View where _MountedNode == _FunctionNode { +public protocol __FunctionView: _Mountable, View { associatedtype __ViewState static func __initializeState(from view: borrowing Self) -> __ViewState @@ -18,8 +18,8 @@ public extension __FunctionView { _ view: consuming Self, context: borrowing _ViewContext, ctx: inout _MountContext - ) -> _MountedNode { - .init( + ) -> _FunctionNode { + _FunctionNode( value: view, context: context, ctx: &ctx @@ -28,14 +28,14 @@ public extension __FunctionView { static func _patchNode( _ view: consuming Self, - node: inout _MountedNode, + node: inout _FunctionNode, tx: inout _TransactionContext ) { node.patch(view, tx: &tx) } } -public extension __FunctionView where __ViewState == Void { +public extension __FunctionView { static func __initializeState(from view: borrowing Self) {} static func __restoreState(_ storage: __ViewState, in view: inout Self) {} } diff --git a/Sources/ElementaryUI/Views/View+Mountable.swift b/Sources/ElementaryUI/Views/View+Mountable.swift index 8ef63a2b..d8a390e1 100644 --- a/Sources/ElementaryUI/Views/View+Mountable.swift +++ b/Sources/ElementaryUI/Views/View+Mountable.swift @@ -58,7 +58,7 @@ public protocol View: HTML & _Mountable where Body: HTML & _Mountable { } public protocol _Mountable { - associatedtype _MountedNode: _Reconcilable + associatedtype _MountedNode: _Reconcilable & ~Copyable static func _makeNode( _ view: consuming Self, diff --git a/Sources/ElementaryUIMacros/ViewMacro.swift b/Sources/ElementaryUIMacros/ViewMacro.swift index a1b01edc..8dfd630b 100644 --- a/Sources/ElementaryUIMacros/ViewMacro.swift +++ b/Sources/ElementaryUIMacros/ViewMacro.swift @@ -104,7 +104,7 @@ extension ViewMacro: ExtensionMacro { let extensionDecl: DeclSyntax = """ extension \(raw: type.trimmedDescription): __FunctionView { - //\(access)typealias _MountedNode = _FunctionNode + //\(access)typealias _MountedNode = _FunctionNode \(raw: decls.map { $0.description }.joined(separator: "\n")) } From 853ab9a2e459afe7f17b93ae177240930d55a7dc Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:06:10 +0200 Subject: [PATCH 2/3] WIP: noncopyable nodes --- .../Reconciling/MountContainer.swift | 16 ++++++++-------- .../ElementaryUI/Reconciling/_MountContext.swift | 2 +- .../ElementaryUI/Reconciling/_Reconcilable.swift | 2 +- .../StructureViews/PlaceholderContentView.swift | 2 +- .../ElementaryUI/StructureViews/_KeyedNode.swift | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/ElementaryUI/Reconciling/MountContainer.swift b/Sources/ElementaryUI/Reconciling/MountContainer.swift index 58cca37f..e2ffc2f9 100644 --- a/Sources/ElementaryUI/Reconciling/MountContainer.swift +++ b/Sources/ElementaryUI/Reconciling/MountContainer.swift @@ -120,7 +120,7 @@ final class MountContainer { keys newKeys: borrowing Span<_ViewKey>, tx: inout _TransactionContext, makeNode: @escaping (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, - patchNode: (Int, AnyReconcilable, inout _TransactionContext) -> Void + patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { pendingMakeNode = makeNode patchPrepared(keys: newKeys, tx: &tx, patchNode: patchNode) @@ -130,17 +130,17 @@ final class MountContainer { key newKey: _ViewKey, tx: inout _TransactionContext, makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, - patchNode: (AnyReconcilable, inout _TransactionContext) -> Void + 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) { _, node, tx in patchNode(&node, &tx) } } private func patchPrepared( keys: borrowing Span<_ViewKey>, tx: inout _TransactionContext, - patchNode: (Int, AnyReconcilable, inout _TransactionContext) -> Void + patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { prepareLaneCapacities(newCount: keys.count) @@ -327,21 +327,21 @@ extension MountContainer { newKeyIndex: Int, tx: inout _TransactionContext, containerHandle: LayoutContainer.Handle?, - patchNode: (Int, AnyReconcilable, inout _TransactionContext) -> Void + patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { let state = takeState() switch consume state { case .pending: setPending(transaction: tx.transaction, newKeyIndex: newKeyIndex) - case .mounted(let mounted): - patchNode(newKeyIndex, mounted.node, &tx) + case .mounted(var mounted): + patchNode(newKeyIndex, &mounted.node, &tx) setMounted(mounted) case .reviving(var mounted): mounted.transitionCoordinator?.cancelRemoval(tx: &tx) mounted.didMove = true Self.reportReenteringElements(of: mounted, handle: containerHandle, tx: &tx) - patchNode(newKeyIndex, mounted.node, &tx) + patchNode(newKeyIndex, &mounted.node, &tx) setMounted(mounted) case .removed: preconditionFailure("active lane contains removed slot") diff --git a/Sources/ElementaryUI/Reconciling/_MountContext.swift b/Sources/ElementaryUI/Reconciling/_MountContext.swift index 4470bcfd..92a5ee08 100644 --- a/Sources/ElementaryUI/Reconciling/_MountContext.swift +++ b/Sources/ElementaryUI/Reconciling/_MountContext.swift @@ -67,7 +67,7 @@ public struct _MountContext: ~Copyable, ~Escapable { } } - mutating func withTransitionBoundary(_ body: (inout _MountContext) -> R) -> R { + mutating func withTransitionBoundary(_ body: (inout _MountContext) -> R) -> R { let previousIsRoot = isRoot isRoot = false let result = body(&self) diff --git a/Sources/ElementaryUI/Reconciling/_Reconcilable.swift b/Sources/ElementaryUI/Reconciling/_Reconcilable.swift index 03f8f42a..6fa9a7d2 100644 --- a/Sources/ElementaryUI/Reconciling/_Reconcilable.swift +++ b/Sources/ElementaryUI/Reconciling/_Reconcilable.swift @@ -4,7 +4,7 @@ public protocol _Reconcilable: ~Copyable { consuming func unmount(_ context: inout _CommitContext) } -struct AnyReconcilable { +struct AnyReconcilable: ~Copyable { class _Box { func unmount(_ context: inout _CommitContext) {} } diff --git a/Sources/ElementaryUI/StructureViews/PlaceholderContentView.swift b/Sources/ElementaryUI/StructureViews/PlaceholderContentView.swift index 607945ea..710051ac 100644 --- a/Sources/ElementaryUI/StructureViews/PlaceholderContentView.swift +++ b/Sources/ElementaryUI/StructureViews/PlaceholderContentView.swift @@ -43,7 +43,7 @@ extension PlaceholderContentView: _Mountable { public final class _PlaceholderNode: _Reconcilable { var node: AnyReconcilable - init(node: AnyReconcilable) { + init(node: consuming AnyReconcilable) { self.node = node } diff --git a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift index 6bb1a667..561a25b5 100644 --- a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift +++ b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift @@ -35,7 +35,7 @@ public struct _KeyedNode: ~Copyable, _Reconcilable { key: _ViewKey, context: inout _TransactionContext, makeNode: @escaping (borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, - patchNode: (AnyReconcilable, inout _TransactionContext) -> Void + patchNode: (inout AnyReconcilable, inout _TransactionContext) -> Void ) { container.patch( key: key, @@ -49,7 +49,7 @@ public struct _KeyedNode: ~Copyable, _Reconcilable { _ newKeys: borrowing Span<_ViewKey>, context: inout _TransactionContext, makeNode: @escaping (Int, borrowing _ViewContext, inout _MountContext) -> AnyReconcilable, - patchNode: (Int, AnyReconcilable, inout _TransactionContext) -> Void + patchNode: (Int, inout AnyReconcilable, inout _TransactionContext) -> Void ) { container.patch( keys: newKeys, From c4a77a2636e681f7b5c964d1d7eb34e4d67f8f7b Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:10:41 +0200 Subject: [PATCH 3/3] noncopyable function node --- .../Reconciling/PendingFunctionQueue.swift | 6 +- .../Reconciling/SchedulableNode.swift | 27 ++ .../ElementaryUI/Reconciling/Scheduler.swift | 22 +- .../Reconciling/_TransactionContext.swift | 4 +- .../StructureViews/_ForEachNode.swift | 28 +- .../Views/Function/_FunctionNode.swift | 264 +++++++++++------- .../Views/Function/_FunctionView.swift | 6 +- Sources/ElementaryUIMacros/ViewMacro.swift | 2 - Sources/Reactivity/WithTracking.swift | 10 +- 9 files changed, 216 insertions(+), 153 deletions(-) create mode 100644 Sources/ElementaryUI/Reconciling/SchedulableNode.swift diff --git a/Sources/ElementaryUI/Reconciling/PendingFunctionQueue.swift b/Sources/ElementaryUI/Reconciling/PendingFunctionQueue.swift index cbade69e..15a29dbc 100644 --- a/Sources/ElementaryUI/Reconciling/PendingFunctionQueue.swift +++ b/Sources/ElementaryUI/Reconciling/PendingFunctionQueue.swift @@ -1,12 +1,12 @@ import BasicContainers struct PendingFunctionQueue: ~Copyable { - private var functionsToRun: UniqueArray<(AnyFunctionNode, Transaction?)> = .init() + private var functionsToRun: UniqueArray<(_SchedulableNode, Transaction?)> = .init() var isEmpty: Bool { functionsToRun.isEmpty } // TODO: add transaction here? - mutating func registerFunctionForUpdate(_ node: AnyFunctionNode, transaction: Transaction?) { + mutating func registerFunctionForUpdate(_ node: _SchedulableNode, transaction: Transaction?) { logTrace("registering function run \(node.identifier)") // sorted insert by depth in reverse order, avoiding duplicates var inserted = false @@ -30,7 +30,7 @@ struct PendingFunctionQueue: ~Copyable { } } - mutating func next() -> (AnyFunctionNode, Transaction?)? { + mutating func next() -> (_SchedulableNode, Transaction?)? { functionsToRun.popLast() } diff --git a/Sources/ElementaryUI/Reconciling/SchedulableNode.swift b/Sources/ElementaryUI/Reconciling/SchedulableNode.swift new file mode 100644 index 00000000..4ff1a940 --- /dev/null +++ b/Sources/ElementaryUI/Reconciling/SchedulableNode.swift @@ -0,0 +1,27 @@ +import Reactivity + +public class _SchedulableNode { + let depthInTree: Int + var trackingSession: TrackingSession? + + init(depthInTree: Int) { + self.depthInTree = depthInTree + } + + var identifier: ObjectIdentifier { ObjectIdentifier(self) } + + func runUpdate(tx: inout _TransactionContext) {} + + func progressAnimation(tx: inout _TransactionContext) -> AnimationProgressResult { + .completed + } + + func startTracking(for accessList: ReactivePropertyAccessList, scheduler: Scheduler) { + let session = ReactiveTrackingSession() + session.trackWillSet(for: accessList) { + [self, scheduler] _ in + scheduler.invalidateFunction(self) + } + self.trackingSession = TrackingSession(session.cancel) + } +} diff --git a/Sources/ElementaryUI/Reconciling/Scheduler.swift b/Sources/ElementaryUI/Reconciling/Scheduler.swift index 819474d8..800f6f91 100644 --- a/Sources/ElementaryUI/Reconciling/Scheduler.swift +++ b/Sources/ElementaryUI/Reconciling/Scheduler.swift @@ -1,20 +1,10 @@ import BasicContainers -struct AnyFunctionNode { - let identifier: ObjectIdentifier - let depthInTree: Int - let runUpdate: (inout _TransactionContext) -> Void -} - enum AnimationProgressResult { case stillRunning case completed } -struct AnyAnimatable { - let progressAnimation: (inout _TransactionContext) -> AnimationProgressResult -} - enum CommitAction { case patchText(node: DOM.Node, text: String) case patchAttributes(node: DOM.Node, from: _AttributeStorage, to: _AttributeStorage) @@ -49,7 +39,7 @@ final class Scheduler { private var pendingUpdates: UniqueArray<(inout _TransactionContext) -> Void> = .init() private var pendingCommitActions: UniqueArray = .init() private var pendingEffects: UniqueArray<() -> Void> = .init() - private var runningAnimations: [AnyAnimatable] = [] + private var runningAnimations: [_SchedulableNode] = [] // Scheduling state // True while an update cycle is either scheduled or currently running. @@ -88,7 +78,7 @@ final class Scheduler { // MARK: - Public API - func invalidateFunction(_ function: AnyFunctionNode) { + func invalidateFunction(_ function: _SchedulableNode) { if ambientContext != nil { ambientContext!.addFunction(function) return @@ -124,8 +114,8 @@ final class Scheduler { ensureUpdateCycleScheduled() } - func registerAnimation(_ animation: AnyAnimatable) { - runningAnimations.append(animation) + func registerAnimation(_ node: _SchedulableNode) { + runningAnimations.append(node) ensureAnimationFrameScheduled() } @@ -286,8 +276,8 @@ final class Scheduler { transaction: transaction ) - runningAnimations.removeAll(where: { animation in - let result = animation.progressAnimation(&context) + runningAnimations.removeAll(where: { node in + let result = node.progressAnimation(tx: &context) return result == .completed }) diff --git a/Sources/ElementaryUI/Reconciling/_TransactionContext.swift b/Sources/ElementaryUI/Reconciling/_TransactionContext.swift index 9f7f13a1..48de542a 100644 --- a/Sources/ElementaryUI/Reconciling/_TransactionContext.swift +++ b/Sources/ElementaryUI/Reconciling/_TransactionContext.swift @@ -17,7 +17,7 @@ public struct _TransactionContext: ~Copyable { self.transaction = transaction ?? .init() } - mutating func addFunction(_ function: AnyFunctionNode) { + mutating func addFunction(_ function: _SchedulableNode) { pendingFunctions.registerFunctionForUpdate(function, transaction: transaction) } @@ -32,7 +32,7 @@ public struct _TransactionContext: ~Copyable { consuming func drain() { while let (node, transaction) = pendingFunctions.next() { self.transaction = transaction ?? .init() - node.runUpdate(&self) + node.runUpdate(tx: &self) } } } diff --git a/Sources/ElementaryUI/StructureViews/_ForEachNode.swift b/Sources/ElementaryUI/StructureViews/_ForEachNode.swift index bec645e1..1d5cdb68 100644 --- a/Sources/ElementaryUI/StructureViews/_ForEachNode.swift +++ b/Sources/ElementaryUI/StructureViews/_ForEachNode.swift @@ -2,13 +2,11 @@ import BasicContainers import Reactivity // TODO: refactor this entire thing - we need to make the key list without the views, and the resolve the views just off the data.... -public final class _ForEachNode: _Reconcilable +public final class _ForEachNode: _SchedulableNode, _Reconcilable where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable { private var data: Data private var contentBuilder: @Sendable (Data.Element) -> Content - private var trackingSession: TrackingSession? = nil private var container: MountContainer! - private var asFunctionNode: AnyFunctionNode! private var keysScratch: UniqueArray<_ViewKey> = .init() private var viewsScratch: [Content] = .init() // TODO: remove this once we refactored everything @@ -22,7 +20,7 @@ where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable { self.data = data self.contentBuilder = contentBuilder - self.asFunctionNode = AnyFunctionNode(self, depthInTree: context.functionDepth) + super.init(depthInTree: context.functionDepth) let session = evaluateViewsAndKeys(scheduler: ctx.scheduler) @@ -50,7 +48,11 @@ where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable { runFunction(tx: &tx) } - borrowing func runFunction(tx: inout _TransactionContext) { + override func runUpdate(tx: inout _TransactionContext) { + runFunction(tx: &tx) + } + + func runFunction(tx: inout _TransactionContext) { self.trackingSession.take()?.cancel() let session = evaluateViewsAndKeys( @@ -59,6 +61,10 @@ where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable { self.trackingSession = session + patchContainer(tx: &tx) + } + + private borrowing func patchContainer(tx: inout _TransactionContext) { container.patch( keys: self.keysScratch.span, tx: &tx, @@ -96,18 +102,10 @@ where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable { self.viewsScratch.append(view) self.keysScratch.append(view._key) } - } onWillSet: { [scheduler, asFunctionNode] in - scheduler.invalidateFunction(asFunctionNode) + } onWillSet: { [scheduler, self] in + scheduler.invalidateFunction(self) } return session } } - -extension AnyFunctionNode { - init(_ node: _ForEachNode, depthInTree: Int) { - self.identifier = ObjectIdentifier(node) - self.depthInTree = depthInTree - self.runUpdate = node.runFunction - } -} diff --git a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift index 12f3ec3b..a91a6488 100644 --- a/Sources/ElementaryUI/Views/Function/_FunctionNode.swift +++ b/Sources/ElementaryUI/Views/Function/_FunctionNode.swift @@ -1,142 +1,196 @@ import Reactivity -// TODO: find a better name for this, "function node" is weird terminology - -// NOTE: ChildNode must be specified as extra argument to avoid a compiler error in embedded -// FIXME: embedded - try with embedded main-snapshot build, revert extra argument if it works -public final class _FunctionNode -where Value: __FunctionView { - private var state: Value.__ViewState? - private var value: Value? - private var context: _ViewContext? - private var animatedValue: AnimatedValue - private var trackingSession: TrackingSession? = nil - - public var depthInTree: Int - - var asFunctionNode: AnyFunctionNode! - - public var identifier: String { - "\(depthInTree):\(ObjectIdentifier(self).hashValue)" +// FIXME EMBEDDED: this is a hack to get the goshdarn Value.Body._MountedNode to work in embedded +// TODO: report swiftlang github issue +public typealias _FunctionNode = __FunctionNode + +public struct __FunctionNode: ~Copyable, _Reconcilable +where ChildNode == Value.Body._MountedNode { + let context: _ViewContext + let depthInTree: Int + var state: Value.__ViewState + var lastValue: Value + + var storage: Storage + + enum Storage: ~Copyable { + case inline(ChildNode) + case box(SchedulableFunction) } - var child: Value.Body._MountedNode? - - init( - value: consuming Value, - context: borrowing _ViewContext, - ctx: inout _MountContext - ) { + init(value: consuming Value, context: borrowing _ViewContext, ctx: inout _MountContext) { self.depthInTree = context.functionDepth - self.state = Value.__initializeState(from: value) - self.animatedValue = AnimatedValue(value: Value.__getAnimatableData(from: value)) + + // TODO: make this better, this is weird + var childContext = copy context + childContext.functionDepth += 1 + self.context = childContext + Value.__applyContext(context, to: &value) - Value.__restoreState(state!, in: &value) + Value.__restoreState(self.state, in: &value) - self.value = copy value - self.context = copy context - self.context!.functionDepth += 1 + let (body, accessList) = withAccessTracking { value.body } - self.asFunctionNode = AnyFunctionNode(self) + self.lastValue = consume value - logTrace("added function \(identifier)") + let childNode = Value.Body._makeNode(body, context: self.context, ctx: &ctx) - let (newContent, session) = withReactiveTrackingSession { - value.body - } onWillSet: { [scheduler = ctx.scheduler, asFunctionNode = asFunctionNode!] in - scheduler.invalidateFunction(asFunctionNode) + let animatableData = Value.__getAnimatableData(from: self.lastValue) + if accessList != nil || !animatableData.isEmpty { + let s = Storage.makeBox( + child: childNode, + value: self.lastValue, + depthInTree: self.depthInTree, + accessList: accessList, + animatableData: animatableData, + scheduler: ctx.scheduler + ) + self.storage = .box(s) + } else { + self.storage = .inline(childNode) } - - self.trackingSession = session - self.child = Value.Body._makeNode(newContent, context: self.context!, ctx: &ctx) - self.value = value } - func patch(_ value: consuming Value, tx: inout _TransactionContext) { - precondition(self.value != nil, "value must be set") - precondition(self.context != nil, "context must be set") + mutating func patch(_ newValue: consuming Value, tx: inout _TransactionContext) { + guard !Value.__areEqual(a: newValue, b: lastValue) else { + return + } - let needsRerender = !Value.__areEqual(a: value, b: self.value!) + Value.__applyContext(self.context, to: &newValue) + Value.__restoreState(self.state, in: &newValue) - // NOTE: the idea is that way always store a "wired-up" value, so that we can re-run the function for free - // the equality check is generated to exclude State and Context from the equality check - Value.__applyContext(self.context!, to: &value) - Value.__restoreState(state!, in: &value) - self.value = value + storage.patch(newValue, depthInTree: depthInTree, tx: &tx) - if needsRerender { - let didStartAnimation = animatedValue.setValueAndReturnIfAnimationWasStarted( - Value.__getAnimatableData(from: self.value!), - transaction: tx.transaction, - frameTime: tx.currentFrameTime - ) + lastValue = consume newValue + } - if didStartAnimation == true { - tx.scheduler.registerAnimation(.init(progressAnimation: self.progressAnimation(_:))) - } + public consuming func unmount(_ context: inout _CommitContext) { + switch storage { + case .inline(var child): + child.unmount(&context) + case .box(let s): + s.trackingSession.take()?.cancel() + s.unmountChild(&context) + s.animatedValue.cancelAnimation() + } + } +} - tx.addFunction(asFunctionNode) +// MARK: - Storage + +extension __FunctionNode.Storage where ChildNode: ~Copyable { + static func makeBox( + child: consuming ChildNode, + value: borrowing Value, + depthInTree: Int, + accessList: ReactivePropertyAccessList?, + animatableData: AnimatableVector, + scheduler: Scheduler + ) -> SchedulableFunction { + let s = SchedulableFunction( + child: child, + animatedValue: AnimatedValue(value: animatableData), + wiredValue: copy value, + depthInTree: depthInTree + ) + if let accessList { + s.startTracking(for: accessList, scheduler: scheduler) } + return s } - func progressAnimation(_ tx: inout _TransactionContext) -> AnimationProgressResult { - assert(!animatedValue.model.isEmpty, "animation should never be called without an animatable value") - guard animatedValue.isAnimating else { return .completed } + mutating func patch( + _ value: borrowing Value, + depthInTree: Int, + tx: inout _TransactionContext + ) { + switch self { + case .inline(var child): + let v = copy value + let (body, accessList) = withAccessTracking { v.body } + Value.Body._patchNode(body, node: &child, tx: &tx) + + let animatableData = Value.__getAnimatableData(from: value) + if accessList != nil || !animatableData.isEmpty { + let s = Self.makeBox( + child: child, + value: value, + depthInTree: depthInTree, + accessList: accessList, + animatableData: animatableData, + scheduler: tx.scheduler + ) + self = .box(s) + } else { + self = .inline(child) + } - animatedValue.progressToTime(tx.currentFrameTime) - runFunction(tx: &tx) + case .box(let s): + s.trackingSession.take()?.cancel() + + let didStartAnimation = s.animatedValue + .setValueAndReturnIfAnimationWasStarted( + Value.__getAnimatableData(from: value), + transaction: tx.transaction, + frameTime: tx.currentFrameTime + ) + if didStartAnimation { + tx.scheduler.registerAnimation(s) + } - return animatedValue.isAnimating ? .stillRunning : .completed + s.wiredValue = copy value + s.runUpdate(tx: &tx) + self = .box(s) + } } +} - func runFunction(tx: inout _TransactionContext) { - logTrace("running function \(identifier)") - - precondition(self.value != nil, "value must be set") - precondition(self.context != nil, "context must be set") +final class SchedulableFunction< + Value: __FunctionView, + Child: _Mountable, + ChildNode: _Reconcilable & ~Copyable +>: _SchedulableNode +where Child == Value.Body, ChildNode == Child._MountedNode { + var child: ChildNode? + var animatedValue: AnimatedValue + var wiredValue: Value + var patchChild: (consuming Child, inout ChildNode, inout _TransactionContext) -> Void - // create a copy of the value to avoid mutating the original value, especially for animations - var value = self.value! + init( + child: consuming ChildNode, + animatedValue: consuming AnimatedValue, + wiredValue: Value, + depthInTree: Int + ) { + self.child = .some(child) + self.animatedValue = animatedValue + self.wiredValue = wiredValue + self.patchChild = Child._patchNode + super.init(depthInTree: depthInTree) + } + override func runUpdate(tx: inout _TransactionContext) { + var v = wiredValue if !animatedValue.model.isEmpty { - Value.__setAnimatableData(animatedValue.presentation.animatableVector, to: &value) + Value.__setAnimatableData(animatedValue.presentation.animatableVector, to: &v) } - - self.trackingSession.take()?.cancel() - - let (newContent, session) = withReactiveTrackingSession { - value.body - } onWillSet: { [scheduler = tx.scheduler, asFunctionNode = asFunctionNode!] in - scheduler.invalidateFunction(asFunctionNode) + let (body, accessList) = withAccessTracking { v.body } + if let accessList { + startTracking(for: accessList, scheduler: tx.scheduler) } - - self.trackingSession = session - - precondition(child != nil, "function child is missing during transaction update") - Value.Body._patchNode(newContent, node: &child!, tx: &tx) + patchChild(body, &child!, &tx) } -} - -extension _FunctionNode: _Reconcilable { - public consuming func unmount(_ context: inout _CommitContext) { - self.trackingSession.take()?.cancel() - let c = self.child.take() - c?.unmount(&context) - - self.animatedValue.cancelAnimation() - self.state = nil - self.value = nil - self.context = nil - self.asFunctionNode = nil + override func progressAnimation(tx: inout _TransactionContext) -> AnimationProgressResult { + guard animatedValue.isAnimating else { return .completed } + animatedValue.progressToTime(tx.currentFrameTime) + trackingSession.take()?.cancel() + runUpdate(tx: &tx) + return animatedValue.isAnimating ? .stillRunning : .completed } -} -extension AnyFunctionNode { - init(_ function: _FunctionNode) { - self.identifier = ObjectIdentifier(function) - self.depthInTree = function.depthInTree - self.runUpdate = function.runFunction + func unmountChild(_ context: inout _CommitContext) { + child.take()?.unmount(&context) } } diff --git a/Sources/ElementaryUI/Views/Function/_FunctionView.swift b/Sources/ElementaryUI/Views/Function/_FunctionView.swift index ae36f9fe..a593c0c8 100644 --- a/Sources/ElementaryUI/Views/Function/_FunctionView.swift +++ b/Sources/ElementaryUI/Views/Function/_FunctionView.swift @@ -19,11 +19,7 @@ public extension __FunctionView { context: borrowing _ViewContext, ctx: inout _MountContext ) -> _FunctionNode { - _FunctionNode( - value: view, - context: context, - ctx: &ctx - ) + _FunctionNode(value: view, context: context, ctx: &ctx) } static func _patchNode( diff --git a/Sources/ElementaryUIMacros/ViewMacro.swift b/Sources/ElementaryUIMacros/ViewMacro.swift index 8dfd630b..76412cee 100644 --- a/Sources/ElementaryUIMacros/ViewMacro.swift +++ b/Sources/ElementaryUIMacros/ViewMacro.swift @@ -104,8 +104,6 @@ extension ViewMacro: ExtensionMacro { let extensionDecl: DeclSyntax = """ extension \(raw: type.trimmedDescription): __FunctionView { - //\(access)typealias _MountedNode = _FunctionNode - \(raw: decls.map { $0.description }.joined(separator: "\n")) } """ diff --git a/Sources/Reactivity/WithTracking.swift b/Sources/Reactivity/WithTracking.swift index dfe47fd2..1d8ec864 100644 --- a/Sources/Reactivity/WithTracking.swift +++ b/Sources/Reactivity/WithTracking.swift @@ -1,4 +1,4 @@ -struct ReactivePropertyAccessList: Sendable { +package struct ReactivePropertyAccessList: Sendable { struct Entry: Sendable { var tracker: ReactivityTracker var properties: Set = [] @@ -26,7 +26,7 @@ struct ReactivePropertyAccessList: Sendable { } } -struct ReactiveTrackingSession: Sendable { +package struct ReactiveTrackingSession: Sendable { private struct State { var subscriptions: [ReactivityTracker.SubscriptionToken] = [] var isCancelled = false @@ -55,7 +55,7 @@ struct ReactiveTrackingSession: Sendable { } } -private func withAccessTracking(_ block: () -> T) -> (T, ReactivePropertyAccessList?) { +package func withAccessTracking(_ block: () -> T) -> (T, ReactivePropertyAccessList?) { var accessList: ReactivePropertyAccessList? let result = withUnsafeMutablePointer(to: &accessList) { ptr in @@ -78,7 +78,7 @@ private func withAccessTracking(_ block: () -> T) -> (T, ReactivePropertyAcce } extension ReactiveTrackingSession { - func trackWillSet(for accessList: consuming ReactivePropertyAccessList, _ observer: @Sendable @escaping (PropertyID) -> Void) { + package func trackWillSet(for accessList: consuming ReactivePropertyAccessList, _ observer: @Sendable @escaping (PropertyID) -> Void) { add( subscriptions: accessList.entries.values.map { entry in entry.tracker.registerTracking(for: entry.properties, willSet: observer) @@ -154,7 +154,7 @@ package struct TrackingSession { init() {} - init(_ cancel: @escaping () -> Void) { + package init(_ cancel: @escaping () -> Void) { _cancel = cancel }