diff --git a/Sources/ElementaryUI/HTMLViews/ElementModifiers/LayoutObserver.swift b/Sources/ElementaryUI/HTMLViews/ElementModifiers/LayoutObserver.swift
index db37d16..e1ca4a3 100644
--- a/Sources/ElementaryUI/HTMLViews/ElementModifiers/LayoutObserver.swift
+++ b/Sources/ElementaryUI/HTMLViews/ElementModifiers/LayoutObserver.swift
@@ -7,6 +7,10 @@ protocol DOMLayoutObserver: Unmountable {
struct DOMLayoutObservers {
private var storage: [any DOMLayoutObserver] = []
+ var isEmpty: Bool {
+ storage.isEmpty
+ }
+
mutating func add(_ observer: any DOMLayoutObserver) {
storage.append(observer)
}
diff --git a/Sources/ElementaryUI/HTMLViews/_ElementNode.swift b/Sources/ElementaryUI/HTMLViews/_ElementNode.swift
index ce2ddd8..a5fd71f 100644
--- a/Sources/ElementaryUI/HTMLViews/_ElementNode.swift
+++ b/Sources/ElementaryUI/HTMLViews/_ElementNode.swift
@@ -15,10 +15,24 @@ public struct _ElementNode: _Reconcilable {
ctx: inout _MountContext,
makeChild: (borrowing _ViewContext, inout _MountContext) -> Child
) {
- var childContext = copy viewContext
-
let domNode = ctx.dom.createElement(tag)
+ ctx.appendStaticElement(domNode)
+
+ guard !viewContext.hasNoUpstreamModifiers else {
+ // no upstream: apply attributes directly, skip context copy and _AttributeModifier check
+ ctx.dom.addHTMLAttributes(domNode, attributes)
+ self.attributes = .inline(node: domNode, lastApplied: attributes)
+ self.child = ctx.withChildContext { (mctx: consuming _MountContext) in
+ let child = makeChild(viewContext, &mctx)
+ _ = mctx.mountInDOMNode(domNode, observers: [])
+ return child
+ }
+ return
+ }
+
+ var childContext = copy viewContext
+
if childContext.modifiers[_AttributeModifier.key] != nil {
// upstream modifier exists: chain through _AttributeModifier as before
let modifier = _AttributeModifier(value: attributes, upstream: childContext.modifiers)
@@ -39,8 +53,6 @@ public struct _ElementNode: _Reconcilable {
self.mountedModifiers.append(modifier.mount(domNode, &ctx))
}
- ctx.appendStaticElement(domNode)
-
self.child = ctx.withChildContext { (mctx: consuming _MountContext) in
let child = makeChild(childContext, &mctx)
_ = mctx.mountInDOMNode(domNode, observers: layoutObservers) //NOTE: maybe hold on to the container?
@@ -58,10 +70,7 @@ public struct _ElementNode: _Reconcilable {
modifier.updateValue(attributes, &context)
case .inline(let node, let lastApplied):
if attributes != lastApplied {
- let (prev, next, n) = (lastApplied, attributes, node)
- context.scheduler.addCommitAction { ctx in
- ctx.dom.applyHTMLAttributes(n, from: prev, to: next)
- }
+ context.scheduler.addCommitAction(.patchAttributes(node: node, from: lastApplied, to: attributes))
self.attributes = .inline(node: node, lastApplied: attributes)
}
}
@@ -77,3 +86,9 @@ public struct _ElementNode: _Reconcilable {
mountedModifiers.removeAll()
}
}
+
+private extension _ViewContext {
+ var hasNoUpstreamModifiers: Bool {
+ modifiers.isEmpty && layoutObservers.isEmpty
+ }
+}
diff --git a/Sources/ElementaryUI/HTMLViews/_TextNode.swift b/Sources/ElementaryUI/HTMLViews/_TextNode.swift
index d95f2a1..0cddc18 100644
--- a/Sources/ElementaryUI/HTMLViews/_TextNode.swift
+++ b/Sources/ElementaryUI/HTMLViews/_TextNode.swift
@@ -12,9 +12,7 @@ public struct _TextNode: _Reconcilable {
guard !value.utf8Equals(newValue) else { return }
self.value = newValue
- tx.scheduler.addCommitAction { [self] ctx in
- ctx.dom.patchText(domNode, with: value)
- }
+ tx.scheduler.addCommitAction(.patchText(node: domNode, text: value))
}
public consuming func unmount(_ context: inout _CommitContext) {
diff --git a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift
index 2e0ec6f..566b901 100644
--- a/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift
+++ b/Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift
@@ -1,6 +1,7 @@
+import BasicContainers
+
final class ApplicationRuntime {
- private var rootChild: AnyReconcilable?
- private var rootContainer: LayoutContainer?
+ private var rootNode: _ConditionalNode?
private var scheduler: Scheduler
init(dom: DOMInteractor) {
@@ -16,43 +17,40 @@ final class ApplicationRuntime {
tx.withModifiedTransaction {
$0.disablesAnimation = true
} run: { tx in
- let rootViewContext = _ViewContext()
- let mountTransaction = tx.transaction
-
- // TODO: clean this up and reuse a mount container
- tx.scheduler.addCommitAction { [self, rootView, rootViewContext] ctx in
- let (child, container) = ctx.withMountContext(transaction: mountTransaction) { (ctx: consuming _MountContext) in
- let child = AnyReconcilable(
- RootView._makeNode(rootView, context: rootViewContext, ctx: &ctx)
- )
- let container = ctx.consumeAsLayoutContainer(domNode: domRoot, observers: [])
- return (child, container)
- }
-
- container.mountInitial(&ctx)
-
- self.rootChild = child
- self.rootContainer = container
+
+ 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
+ }
}
}
}
}
func unmount() {
- guard let rootChild, let rootContainer else { return }
+ guard var rootNode = self.rootNode.take() else { return }
- scheduler.scheduleUpdate { [rootChild, rootContainer] tx in
+ scheduler.scheduleUpdate { tx in
tx.withModifiedTransaction {
$0.disablesAnimation = true
} run: { tx in
- tx.scheduler.addPlacementAction { ctx in
- rootContainer.removeAllChildren(&ctx)
- rootChild.unmount(&ctx)
- }
+ 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) }
}
}
-
- self.rootChild = nil
- self.rootContainer = nil
}
}
diff --git a/Sources/ElementaryUI/Reconciling/LayoutContainer.swift b/Sources/ElementaryUI/Reconciling/LayoutContainer.swift
index 285c369..288b57d 100644
--- a/Sources/ElementaryUI/Reconciling/LayoutContainer.swift
+++ b/Sources/ElementaryUI/Reconciling/LayoutContainer.swift
@@ -34,28 +34,11 @@ final class LayoutContainer {
}
}
- // TODO: I get rid of this...
- func removeAllChildren(_ context: inout _CommitContext) {
- context.scheduler.scratch.withLayoutEntryScratchFrame { scratch in
- var ops = LayoutPass(layoutContainer: self, scratch: consume scratch)
- layoutNodes.collect(into: &ops, context: &context, op: .removed)
- let entryCount = ops.count
-
- ops.consume { entries in
- if entryCount == 1 {
- context.dom.removeChild(entries[unchecked: 0].reference, from: domNode)
- } else if entryCount > 1 {
- context.dom.clearChildren(in: domNode)
- }
- }
- }
- }
-
private func markDirty(_ tx: inout _TransactionContext) {
guard !isDirty else { return }
isDirty = true
- tx.scheduler.addPlacementAction(performLayout(_:))
+ tx.scheduler.addCommitAction(.patchLayout(container: self))
for observer in layoutObservers {
observer.willLayoutChildren(parent: domNode, context: &tx)
}
@@ -73,7 +56,7 @@ final class LayoutContainer {
}
}
- private func performLayout(_ context: inout _CommitContext) {
+ func performLayout(_ context: inout _CommitContext) {
guard isDirty else { return }
isDirty = false
diff --git a/Sources/ElementaryUI/Reconciling/MountContainer.swift b/Sources/ElementaryUI/Reconciling/MountContainer.swift
index 6d76a9d..436cf7f 100644
--- a/Sources/ElementaryUI/Reconciling/MountContainer.swift
+++ b/Sources/ElementaryUI/Reconciling/MountContainer.swift
@@ -106,9 +106,9 @@ final class MountContainer {
removed.unmount(&context)
}
- activeSlots.removeAll(keepingCapacity: true)
- leavingSlots.removeAll(keepingCapacity: true)
- removedNodes.removeAll(keepingCapacity: true)
+ activeSlots.removeAll()
+ leavingSlots.removeAll()
+ removedNodes.removeAll()
containerHandle = nil
}
diff --git a/Sources/ElementaryUI/Reconciling/Scheduler.swift b/Sources/ElementaryUI/Reconciling/Scheduler.swift
index d9f3dd1..819474d 100644
--- a/Sources/ElementaryUI/Reconciling/Scheduler.swift
+++ b/Sources/ElementaryUI/Reconciling/Scheduler.swift
@@ -15,6 +15,26 @@ 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)
+ case patchLayout(container: LayoutContainer)
+ case closure((inout _CommitContext) -> Void)
+
+ func apply(context: inout _CommitContext) {
+ switch self {
+ case let .patchText(node, text):
+ context.dom.patchText(node, with: text)
+ case let .patchAttributes(node, previousAttributes, newAttributes):
+ context.dom.applyHTMLAttributes(node, from: previousAttributes, to: newAttributes)
+ case let .patchLayout(container):
+ container.performLayout(&context)
+ case let .closure(action):
+ action(&context)
+ }
+ }
+}
+
final class Scheduler {
private let dom: any DOM.Interactor
@@ -27,8 +47,7 @@ final class Scheduler {
// Work queues
private var pendingFunctions: PendingFunctionQueue = .init()
private var pendingUpdates: UniqueArray<(inout _TransactionContext) -> Void> = .init()
- private var pendingCommitActions: UniqueArray<(inout _CommitContext) -> Void> = .init()
- private var pendingPlacements: UniqueArray<(inout _CommitContext) -> Void> = .init()
+ private var pendingCommitActions: UniqueArray = .init()
private var pendingEffects: UniqueArray<() -> Void> = .init()
private var runningAnimations: [AnyAnimatable] = []
@@ -55,7 +74,7 @@ final class Scheduler {
}
private var hasCommitWork: Bool {
- !pendingCommitActions.isEmpty || !pendingPlacements.isEmpty
+ !pendingCommitActions.isEmpty
}
private var needsAnimationFrame: Bool {
@@ -90,14 +109,13 @@ final class Scheduler {
pendingUpdates.append(callback)
}
- func addCommitAction(_ action: @escaping (inout _CommitContext) -> Void) {
+ func addCommitAction(_ action: CommitAction) {
assert(isUpdateCycleActive, "Commit actions must be added during an update cycle")
pendingCommitActions.append(action)
}
- func addPlacementAction(_ action: @escaping (inout _CommitContext) -> Void) {
- assert(isUpdateCycleActive, "Placement actions must be added during an update cycle")
- pendingPlacements.append(action)
+ func addCommitAction(_ action: @escaping (inout _CommitContext) -> Void) {
+ addCommitAction(.closure(action))
}
// Effects are run after all pending transactions are committed
@@ -239,20 +257,10 @@ final class Scheduler {
precondition(passes <= maxCommitPasses, "Exceeded \(maxCommitPasses) commit passes - infinite loop?")
if !pendingCommitActions.isEmpty {
- var actions: UniqueArray<(inout _CommitContext) -> Void> = .init()
+ var actions: UniqueArray = .init()
swap(&actions, &pendingCommitActions)
for index in actions.indices {
- actions[index](&context)
- }
- }
-
- if !pendingPlacements.isEmpty {
- var placements: UniqueArray<(inout _CommitContext) -> Void> = .init()
- swap(&placements, &pendingPlacements)
- var index = placements.endIndex
- while index > placements.startIndex {
- index -= 1
- placements[index](&context)
+ actions[index].apply(context: &context)
}
}
}
diff --git a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift
index dfbeb13..dd3f48c 100644
--- a/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift
+++ b/Sources/ElementaryUI/StructureViews/_ConditionalNode.swift
@@ -11,10 +11,9 @@ public struct _ConditionalNode: _Reconcilable {
makeActive: (borrowing _ViewContext, inout _MountContext) -> Node
) {
let initialKey = isA ? keyA : keyB
- let containerContext = copy context
self.container = MountContainer(
mountedKey: initialKey,
- context: consume containerContext,
+ context: context,
ctx: &ctx,
makeNode: makeActive
)
diff --git a/Sources/ElementaryUI/StructureViews/_ForEachNode.swift b/Sources/ElementaryUI/StructureViews/_ForEachNode.swift
index 3224e2c..bec645e 100644
--- a/Sources/ElementaryUI/StructureViews/_ForEachNode.swift
+++ b/Sources/ElementaryUI/StructureViews/_ForEachNode.swift
@@ -28,11 +28,9 @@ where Data: Collection, Content: _KeyReadableView, Content.Value: _Mountable {
self.trackingSession = session
- let containerContext = copy context
-
self.container = MountContainer(
mountedKeyStorage: keysScratch.span,
- context: consume containerContext,
+ context: context,
ctx: &ctx,
makeNode: { index, context, mountCtx in
Content.Value._makeNode(self.viewsScratch[index]._value, context: context, ctx: &mountCtx)
diff --git a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift
index ec88f80..780812b 100644
--- a/Sources/ElementaryUI/StructureViews/_KeyedNode.swift
+++ b/Sources/ElementaryUI/StructureViews/_KeyedNode.swift
@@ -7,10 +7,9 @@ public struct _KeyedNode: _Reconcilable {
ctx: inout _MountContext,
makeNode: (Int, borrowing _ViewContext, inout _MountContext) -> Node
) {
- let containerContext = copy context
self.container = MountContainer(
mountedKeyStorage: keys,
- context: consume containerContext,
+ context: context,
ctx: &ctx,
makeNode: makeNode
)
@@ -23,10 +22,9 @@ public struct _KeyedNode: _Reconcilable {
ctx: inout _MountContext,
makeNode: (borrowing _ViewContext, inout _MountContext) -> Node
) {
- let containerContext = copy context
self.container = MountContainer(
mountedKey: key,
- context: consume containerContext,
+ context: context,
ctx: &ctx,
makeNode: makeNode
)