Tokamak/Sources/TokamakCore/Fiber/Fiber.swift

627 lines
20 KiB
Swift

// Copyright 2022 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 2/15/22.
//
import Foundation
import OpenCombineShim
// swiftlint:disable type_body_length
@_spi(TokamakCore)
public extension FiberReconciler {
/// A manager for a single `View`.
///
/// There are always 2 `Fiber`s for every `View` in the tree,
/// a current `Fiber`, and a work in progress `Fiber`.
/// They point to each other using the `alternate` property.
///
/// The current `Fiber` represents the `View` as it is currently rendered on the screen.
/// The work in progress `Fiber` (the `alternate` of current),
/// is used in the reconciler to compute the new tree.
///
/// When reconciling, the tree is recomputed from
/// the root of the state change on the work in progress `Fiber`.
/// Each node in the fiber tree is updated to apply any changes,
/// and a list of mutations needed to get the rendered output to match is created.
///
/// After the entire tree has been traversed, the current and work in progress trees are swapped,
/// making the updated tree the current one,
/// and leaving the previous current tree available to apply future changes on.
final class Fiber {
weak var reconciler: FiberReconciler<Renderer>?
/// The underlying value behind this `Fiber`. Either a `Scene` or `View` instance.
///
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance,
/// and captures a weak reference to `self` in the visitor function,
/// which requires all stored properties be set before capturing.
@_spi(TokamakCore)
public var content: Content!
/// Outputs from evaluating `View._makeView`
///
/// Stored as an IUO because creating `ViewOutputs` depends on
/// the `bindProperties` method, which requires
/// all stored properties be set before using.
/// `outputs` is guaranteed to be set in the initializer.
var outputs: ViewOutputs!
/// The erased `Layout` to use for this content.
///
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance.
var layout: AnyLayout?
/// The identity of this `View`
var id: Identity?
/// The mounted element, if this is a `Renderer` primitive.
var element: Renderer.ElementType?
/// The index of this element in its `elementParent`
var elementIndex: Int?
/// The first child node.
@_spi(TokamakCore)
public var child: Fiber?
/// This node's right sibling.
@_spi(TokamakCore)
public var sibling: Fiber?
/// An unowned reference to the parent node.
///
/// Parent references are `unowned` (as opposed to `weak`)
/// because the parent will always exist if a child does.
/// If the parent is released, the child is released with it.
@_spi(TokamakCore)
public unowned var parent: Fiber?
/// The nearest parent that can be mounted on.
unowned var elementParent: Fiber?
/// The nearest parent that receives preferences.
unowned var preferenceParent: Fiber?
/// The cached type information for the underlying `View`.
var typeInfo: TypeInfo?
/// Boxes that store `State` data.
var state: [PropertyInfo: MutableStorage] = [:]
/// Subscribed `Cancellable`s keyed with the property contained the observable.
///
/// Each time properties are bound, a new subscription could be created.
/// When the subscription is overridden, the old cancellable is released.
var subscriptions: [PropertyInfo: AnyCancellable] = [:]
/// Storage for `PreferenceKey` values as they are passed up the tree.
var preferences: _PreferenceStore?
/// The computed dimensions and origin.
var geometry: ViewGeometry?
/// The WIP node if this is current, or the current node if this is WIP.
@_spi(TokamakCore)
public weak var alternate: Fiber?
var createAndBindAlternate: (() -> Fiber?)?
/// A box holding a value for an `@State` property wrapper.
/// Will call `onSet` (usually a `Reconciler.reconcile` call) when updated.
final class MutableStorage {
private(set) var value: Any
let onSet: () -> ()
func setValue(_ newValue: Any, with transaction: Transaction) {
value = newValue
onSet()
}
init(initialValue: Any, onSet: @escaping () -> ()) {
value = initialValue
self.onSet = onSet
}
}
public enum Identity: Hashable {
case explicit(AnyHashable)
case structural(index: Int)
}
init<V: View>(
_ view: inout V,
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
elementIndex: Int?,
traits: _ViewTraitStore?,
reconciler: FiberReconciler<Renderer>?
) {
self.reconciler = reconciler
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
typeInfo = TokamakCore.typeInfo(of: V.self)
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &view, typeInfo, environment.environment)
var updateView = view
let viewInputs = ViewInputs(
content: view,
updateContent: { $0(&updateView) },
environment: environment,
traits: traits,
preferenceStore: preferences
)
outputs = V._makeView(viewInputs)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
view = updateView
content = content(for: view)
if let element = element {
self.element = element
} else if Renderer.isPrimitive(view) {
self.element = .init(
from: .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
)
}
if self.element != nil {
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
// Only specify an `elementIndex` if we have an element.
if self.element != nil {
self.elementIndex = elementIndex
}
let alternateView = view
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateView,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
parent: self.parent?.alternate,
elementParent: self.elementParent?.alternate,
preferenceParent: self.preferenceParent?.alternate,
reconciler: reconciler
)
self.alternate = alternate
if self.parent?.child === self {
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
} else {
// Find our left sibling.
var node = self.parent?.child
while node?.sibling !== self {
guard node?.sibling != nil else { return alternate }
node = node?.sibling
}
if node?.sibling === self {
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
}
}
return alternate
}
}
init<V: View>(
bound view: V,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout!,
alternate: Fiber,
outputs: ViewOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
parent: FiberReconciler<Renderer>.Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
if element != nil {
self.layout = layout
}
content = content(for: view)
}
private func bindProperties<T>(
to content: inout T,
_ typeInfo: TypeInfo?,
_ environment: EnvironmentValues
) {
var erased: Any = content
bindProperties(to: &erased, typeInfo, environment)
// swiftlint:disable:next force_cast
content = erased as! T
}
/// Collect `DynamicProperty`s and link their state changes to the reconciler.
private func bindProperties(
to content: inout Any,
_ typeInfo: TypeInfo?,
_ environment: EnvironmentValues
) {
guard let typeInfo = typeInfo else { return }
for property in typeInfo.properties where property.type is DynamicProperty.Type {
var value = property.get(from: content)
// Bind nested properties.
bindProperties(to: &value, TokamakCore.typeInfo(of: property.type), environment)
// Create boxes for `@State` and other mutable properties.
if var storage = value as? WritableValueStorage {
let box = self.state[property] ?? MutableStorage(
initialValue: storage.anyInitialValue,
onSet: { [weak self] in
guard let self = self else { return }
self.reconciler?.fiberChanged(self)
}
)
state[property] = box
storage.getter = { box.value }
storage.setter = { box.setValue($0, with: $1) }
value = storage
// Create boxes for `@StateObject` and other immutable properties.
} else if var storage = value as? ValueStorage {
let box = self.state[property] ?? MutableStorage(
initialValue: storage.anyInitialValue,
onSet: {}
)
state[property] = box
storage.getter = { box.value }
value = storage
// Read from the environment.
} else if var environmentReader = value as? EnvironmentReader {
environmentReader.setContent(from: environment)
value = environmentReader
}
// Subscribe to observable properties.
if let observed = value as? ObservedProperty {
subscriptions[property] = observed.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.reconciler?.fiberChanged(self)
}
}
property.set(value: value, on: &content)
}
if var environmentReader = content as? EnvironmentReader {
environmentReader.setContent(from: environment)
content = environmentReader
}
}
/// Call `update()` on each `DynamicProperty` in the type.
private func updateDynamicProperties(
of content: inout Any,
_ typeInfo: TypeInfo?
) {
guard let typeInfo = typeInfo else { return }
for property in typeInfo.properties where property.type is DynamicProperty.Type {
var value = property.get(from: content)
// Update nested properties.
updateDynamicProperties(of: &value, TokamakCore.typeInfo(of: property.type))
// swiftlint:disable:next force_cast
var dynamicProperty = value as! DynamicProperty
dynamicProperty.update()
property.set(value: dynamicProperty, on: &content)
}
}
/// Update each `DynamicProperty` in our content.
func updateDynamicProperties() {
guard let content = content else { return }
switch content {
case .app(var app, let visit):
updateDynamicProperties(of: &app, typeInfo)
self.content = .app(app, visit: visit)
case .scene(var scene, let visit):
updateDynamicProperties(of: &scene, typeInfo)
self.content = .scene(scene, visit: visit)
case .view(var view, let visit):
updateDynamicProperties(of: &view, typeInfo)
self.content = .view(view, visit: visit)
}
}
func update<V: View>(
with view: inout V,
elementIndex: Int?,
traits: _ViewTraitStore?
) -> Renderer.ElementType.Content? {
typeInfo = TokamakCore.typeInfo(of: V.self)
self.elementIndex = elementIndex
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &view, typeInfo, environment.environment)
var updateView = view
let inputs = ViewInputs(
content: view,
updateContent: {
$0(&updateView)
},
environment: environment,
traits: traits,
preferenceStore: preferences
)
outputs = V._makeView(inputs)
view = updateView
content = content(for: view)
if element != nil {
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
if Renderer.isPrimitive(view) {
return .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
} else {
return nil
}
}
init<A: App>(
_ app: inout A,
rootElement: Renderer.ElementType,
rootEnvironment: EnvironmentValues,
reconciler: FiberReconciler<Renderer>
) {
self.reconciler = reconciler
child = nil
sibling = nil
// `App`s are always the root, so they can have no parent.
parent = nil
elementParent = nil
preferenceParent = nil
element = rootElement
typeInfo = TokamakCore.typeInfo(of: A.self)
bindProperties(to: &app, typeInfo, rootEnvironment)
var updateApp = app
outputs = .init(
inputs: .init(
content: app,
updateContent: {
$0(&updateApp)
},
environment: .init(rootEnvironment),
traits: .init(),
preferenceStore: preferences
)
)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
app = updateApp
content = content(for: app)
layout = .init(RootLayout(renderer: reconciler.renderer))
let alternateApp = app
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateApp,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
reconciler: reconciler
)
self.alternate = alternate
return alternate
}
}
init<A: App>(
bound app: A,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout?,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
parent = nil
elementParent = nil
preferenceParent = nil
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
self.layout = layout
content = content(for: app)
}
init<S: Scene>(
_ scene: inout S,
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
environment: EnvironmentBox?,
reconciler: FiberReconciler<Renderer>?
) {
self.reconciler = reconciler
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.element = element
self.preferenceParent = preferenceParent
typeInfo = TokamakCore.typeInfo(of: S.self)
let environment = environment ?? parent?.outputs.environment ?? .init(.init())
bindProperties(to: &scene, typeInfo, environment.environment)
var updateScene = scene
outputs = S._makeScene(
.init(
content: scene,
updateContent: {
$0(&updateScene)
},
environment: environment,
traits: .init(),
preferenceStore: preferences
)
)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
scene = updateScene
content = content(for: scene)
if element != nil {
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
let alternateScene = scene
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateScene,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
parent: self.parent?.alternate,
elementParent: self.elementParent?.alternate,
preferenceParent: self.preferenceParent?.alternate,
reconciler: reconciler
)
self.alternate = alternate
if self.parent?.child === self {
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
} else {
// Find our left sibling.
var node = self.parent?.child
while node?.sibling !== self {
guard node?.sibling != nil else { return alternate }
node = node?.sibling
}
if node?.sibling === self {
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
}
}
return alternate
}
}
init<S: Scene>(
bound scene: S,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout!,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
parent: FiberReconciler<Renderer>.Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
if element != nil {
self.layout = layout
}
content = content(for: scene)
}
func update<S: Scene>(
with scene: inout S
) -> Renderer.ElementType.Content? {
typeInfo = TokamakCore.typeInfo(of: S.self)
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &scene, typeInfo, environment.environment)
var updateScene = scene
outputs = S._makeScene(.init(
content: scene,
updateContent: {
$0(&updateScene)
},
environment: environment,
traits: .init(),
preferenceStore: preferences
))
scene = updateScene
content = content(for: scene)
if element != nil {
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
return nil
}
}
}