Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
31 changes: 23 additions & 8 deletions Sources/ElementaryUI/HTMLViews/_ElementNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,24 @@ public struct _ElementNode<Child: _Reconcilable>: _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)
Expand All @@ -39,8 +53,6 @@ public struct _ElementNode<Child: _Reconcilable>: _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?
Expand All @@ -58,10 +70,7 @@ public struct _ElementNode<Child: _Reconcilable>: _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)
}
}
Expand All @@ -77,3 +86,9 @@ public struct _ElementNode<Child: _Reconcilable>: _Reconcilable {
mountedModifiers.removeAll()
}
}

private extension _ViewContext {
var hasNoUpstreamModifiers: Bool {
modifiers.isEmpty && layoutObservers.isEmpty
}
}
4 changes: 1 addition & 3 deletions Sources/ElementaryUI/HTMLViews/_TextNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
54 changes: 26 additions & 28 deletions Sources/ElementaryUI/Reconciling/ApplicationRuntime.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import BasicContainers

final class ApplicationRuntime<DOMInteractor: DOM.Interactor> {
private var rootChild: AnyReconcilable?
private var rootContainer: LayoutContainer?
private var rootNode: _ConditionalNode?
private var scheduler: Scheduler

init(dom: DOMInteractor) {
Expand All @@ -16,43 +17,40 @@ final class ApplicationRuntime<DOMInteractor: DOM.Interactor> {
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
}
}
21 changes: 2 additions & 19 deletions Sources/ElementaryUI/Reconciling/LayoutContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -73,7 +56,7 @@ final class LayoutContainer {
}
}

private func performLayout(_ context: inout _CommitContext) {
func performLayout(_ context: inout _CommitContext) {
guard isDirty else { return }
isDirty = false

Expand Down
6 changes: 3 additions & 3 deletions Sources/ElementaryUI/Reconciling/MountContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
46 changes: 27 additions & 19 deletions Sources/ElementaryUI/Reconciling/Scheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<CommitAction> = .init()
private var pendingEffects: UniqueArray<() -> Void> = .init()
private var runningAnimations: [AnyAnimatable] = []

Expand All @@ -55,7 +74,7 @@ final class Scheduler {
}

private var hasCommitWork: Bool {
!pendingCommitActions.isEmpty || !pendingPlacements.isEmpty
!pendingCommitActions.isEmpty
}

private var needsAnimationFrame: Bool {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<CommitAction> = .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)
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/ElementaryUI/StructureViews/_ConditionalNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
4 changes: 1 addition & 3 deletions Sources/ElementaryUI/StructureViews/_ForEachNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions Sources/ElementaryUI/StructureViews/_KeyedNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
Expand Down
Loading