Experimental "React Fiber"-like Reconciler (#471)
* Initial Reconciler using visitor pattern * Preliminary static HTML renderer using the new reconciler * Add environment * Initial DOM renderer * Nearly-working and simplified reconciler * Working reconciler for HTML/DOM renderers * Rename files, and split code across files * Add some documentation and refinements * Remove GraphRendererTests * Re-add Optional.body for StackReconciler-based renderers * Add benchmarks to compare the stack/fiber reconcilers * Fix some issues created for the StackReconciler, and add update benchmarks * Add BenchmarkState.measure to only calculate the time to update * Fix hang in update shallow benchmark * Fix build errors * Address build issues * Remove File.swift headers * Rename Element -> FiberElement and Element.Data -> FiberElement.Content * Add doc comment explaining unowned usage * Add doc comments explaining implicitly unwrapped optionals * Attempt to use Swift instead of JS for applying mutations * Fix issue with not applying updates to DOMFiberElement * Add comment explaining manual implementation of Hashable for PropertyInfo * Fix linter issues * Remove dynamicMember label from subscript * Re-enable carton test * Re-enable TokamakDemo with StackReconciler Co-authored-by: Max Desiatov <max@desiatov.com>
This commit is contained in:
parent
a41ac37500
commit
8177fc8cae
|
@ -22,7 +22,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
with:
|
||||
shell-action: carton test
|
||||
shell-action: carton test --environment node
|
||||
|
||||
# Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
|
||||
# core_macos_build:
|
||||
|
|
|
@ -135,6 +135,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
|
@ -194,6 +195,13 @@ let package = Package(
|
|||
name: "TokamakTestRenderer",
|
||||
dependencies: ["TokamakCore"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakReconcilerTests",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakTests",
|
||||
dependencies: ["TokamakTestRenderer"]
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2021 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.
|
||||
//
|
||||
|
||||
@_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: CustomDebugStringConvertible {
|
||||
weak var reconciler: FiberReconciler<Renderer>?
|
||||
|
||||
/// The underlying `View` instance.
|
||||
///
|
||||
/// Stored as an IUO because we must use the `bindProperties` method
|
||||
/// to create the `View` with its dependencies setup,
|
||||
/// which requires all stored properties be set before using.
|
||||
@_spi(TokamakCore) public var view: Any!
|
||||
/// 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!
|
||||
/// A function to visit `view` generically.
|
||||
///
|
||||
/// Stored as an IUO because it captures a weak reference to `self`, which requires all stored properties be set before capturing.
|
||||
var visitView: ((ViewVisitor) -> ())!
|
||||
/// The identity of this `View`
|
||||
var id: Identity?
|
||||
/// The mounted element, if this is a Renderer primitive.
|
||||
var element: Renderer.ElementType?
|
||||
/// 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.
|
||||
unowned var parent: Fiber?
|
||||
/// The nearest parent that can be mounted on.
|
||||
unowned var elementParent: Fiber?
|
||||
/// The cached type information for the underlying `View`.
|
||||
var typeInfo: TypeInfo?
|
||||
/// Boxes that store `State` data.
|
||||
var state: [PropertyInfo: MutableStorage] = [:]
|
||||
|
||||
/// The WIP node if this is current, or the current node if this is WIP.
|
||||
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?,
|
||||
childIndex: Int,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.reconciler = reconciler
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
typeInfo = TokamakCore.typeInfo(of: V.self)
|
||||
|
||||
let viewInputs = ViewInputs<V>(
|
||||
view: view,
|
||||
proposedSize: parent?.outputs.layoutComputer?.proposeSize(for: view, at: childIndex),
|
||||
environment: parent?.outputs.environment ?? .init(.init())
|
||||
)
|
||||
state = bindProperties(to: &view, typeInfo, viewInputs)
|
||||
self.view = view
|
||||
outputs = V._makeView(viewInputs)
|
||||
visitView = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(self.view as! V)
|
||||
}
|
||||
|
||||
if let element = element {
|
||||
self.element = element
|
||||
} else if Renderer.isPrimitive(view) {
|
||||
self.element = .init(from: .init(from: view))
|
||||
}
|
||||
|
||||
let alternateView = view
|
||||
createAndBindAlternate = {
|
||||
// Create the alternate lazily
|
||||
let alternate = Fiber(
|
||||
bound: alternateView,
|
||||
alternate: self,
|
||||
outputs: self.outputs,
|
||||
typeInfo: self.typeInfo,
|
||||
element: self.element,
|
||||
parent: self.parent?.alternate,
|
||||
elementParent: self.elementParent?.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,
|
||||
alternate: Fiber,
|
||||
outputs: ViewOutputs,
|
||||
typeInfo: TypeInfo?,
|
||||
element: Renderer.ElementType?,
|
||||
parent: FiberReconciler<Renderer>.Fiber?,
|
||||
elementParent: Fiber?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.view = view
|
||||
self.alternate = alternate
|
||||
self.reconciler = reconciler
|
||||
self.element = element
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
self.typeInfo = typeInfo
|
||||
self.outputs = outputs
|
||||
visitView = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(self.view as! V)
|
||||
}
|
||||
}
|
||||
|
||||
private func bindProperties<V: View>(
|
||||
to view: inout V,
|
||||
_ typeInfo: TypeInfo?,
|
||||
_ viewInputs: ViewInputs<V>
|
||||
) -> [PropertyInfo: MutableStorage] {
|
||||
guard let typeInfo = typeInfo else { return [:] }
|
||||
|
||||
var state: [PropertyInfo: MutableStorage] = [:]
|
||||
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
||||
var value = property.get(from: view)
|
||||
if var storage = value as? WritableValueStorage {
|
||||
let box = MutableStorage(initialValue: storage.anyInitialValue, onSet: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.reconciler?.reconcile(from: self)
|
||||
})
|
||||
state[property] = box
|
||||
storage.getter = { box.value }
|
||||
storage.setter = { box.setValue($0, with: $1) }
|
||||
value = storage
|
||||
} else if var environmentReader = value as? EnvironmentReader {
|
||||
environmentReader.setContent(from: viewInputs.environment.environment)
|
||||
value = environmentReader
|
||||
}
|
||||
property.set(value: value, on: &view)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func update<V: View>(
|
||||
with view: inout V,
|
||||
childIndex: Int
|
||||
) -> Renderer.ElementType.Content? {
|
||||
typeInfo = TokamakCore.typeInfo(of: V.self)
|
||||
|
||||
let viewInputs = ViewInputs<V>(
|
||||
view: view,
|
||||
proposedSize: parent?.outputs.layoutComputer?.proposeSize(for: view, at: childIndex),
|
||||
environment: parent?.outputs.environment ?? .init(.init())
|
||||
)
|
||||
state = bindProperties(to: &view, typeInfo, viewInputs)
|
||||
self.view = view
|
||||
outputs = V._makeView(viewInputs)
|
||||
visitView = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(self.view as! V)
|
||||
}
|
||||
|
||||
if Renderer.isPrimitive(view) {
|
||||
return .init(from: view)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
flush()
|
||||
}
|
||||
|
||||
private func flush(level: Int = 0) -> String {
|
||||
let spaces = String(repeating: " ", count: level)
|
||||
return """
|
||||
\(spaces)\(String(describing: typeInfo?.type ?? Any.self)
|
||||
.split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {
|
||||
\(child?.flush(level: level + 2) ?? "")
|
||||
\(spaces)}
|
||||
\(sibling?.flush(level: level) ?? "")
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2021 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.
|
||||
//
|
||||
|
||||
/// A reference type that points to a `Renderer`-specific element that has been mounted.
|
||||
/// For instance, a DOM node in the `DOMFiberRenderer`.
|
||||
public protocol FiberElement: AnyObject {
|
||||
associatedtype Content: FiberElementContent
|
||||
var content: Content { get }
|
||||
init(from content: Content)
|
||||
func update(with content: Content)
|
||||
}
|
||||
|
||||
/// The data used to create an `FiberElement`.
|
||||
///
|
||||
/// We re-use `FiberElement` instances in the `Fiber` tree,
|
||||
/// but can re-create and copy `FiberElementContent` as often as needed.
|
||||
public protocol FiberElementContent: Equatable {
|
||||
init<V: View>(from primitiveView: V)
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright 2021 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.
|
||||
//
|
||||
|
||||
/// A reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
||||
public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||
/// The root node in the `Fiber` tree that represents the `View`s currently rendered on screen.
|
||||
@_spi(TokamakCore) public var current: Fiber!
|
||||
/// The alternate of `current`, or the work in progress tree root.
|
||||
///
|
||||
/// We must keep a strong reference to both the current and alternate tree roots,
|
||||
/// as they only keep weak references to each other.
|
||||
private var alternate: Fiber!
|
||||
/// The `FiberRenderer` used to create and update the `Element`s on screen.
|
||||
public let renderer: Renderer
|
||||
|
||||
public init<V: View>(_ renderer: Renderer, _ view: V) {
|
||||
self.renderer = renderer
|
||||
var view = view.environmentValues(renderer.defaultEnvironment)
|
||||
current = .init(
|
||||
&view,
|
||||
element: renderer.rootElement,
|
||||
parent: nil,
|
||||
elementParent: nil,
|
||||
childIndex: 0,
|
||||
reconciler: self
|
||||
)
|
||||
// Start by building the initial tree.
|
||||
alternate = current.createAndBindAlternate?()
|
||||
reconcile(from: current)
|
||||
}
|
||||
|
||||
/// Convert the first level of children of a `View` into a linked list of `Fiber`s.
|
||||
struct TreeReducer: ViewReducer {
|
||||
final class Result {
|
||||
// For references
|
||||
let fiber: Fiber?
|
||||
let visitChildren: (TreeReducer.Visitor) -> ()
|
||||
unowned var parent: Result?
|
||||
var child: Result?
|
||||
var sibling: Result?
|
||||
var newContent: Renderer.ElementType.Content?
|
||||
|
||||
// For reducing
|
||||
var childrenCount: Int = 0
|
||||
var lastSibling: Result?
|
||||
var nextExisting: Fiber?
|
||||
var nextExistingAlternate: Fiber?
|
||||
|
||||
init(
|
||||
fiber: Fiber?,
|
||||
visitChildren: @escaping (TreeReducer.Visitor) -> (),
|
||||
parent: Result?,
|
||||
child: Fiber?,
|
||||
alternateChild: Fiber?,
|
||||
newContent: Renderer.ElementType.Content? = nil
|
||||
) {
|
||||
self.fiber = fiber
|
||||
self.visitChildren = visitChildren
|
||||
self.parent = parent
|
||||
nextExisting = child
|
||||
nextExistingAlternate = alternateChild
|
||||
self.newContent = newContent
|
||||
}
|
||||
}
|
||||
|
||||
static func reduce<V>(into partialResult: inout Result, nextView: V) where V: View {
|
||||
// Create the node and its element.
|
||||
var nextView = nextView
|
||||
let resultChild: Result
|
||||
if let existing = partialResult.nextExisting {
|
||||
// If a fiber already exists, simply update it with the new view.
|
||||
let newContent = existing.update(
|
||||
with: &nextView,
|
||||
childIndex: partialResult.childrenCount
|
||||
)
|
||||
resultChild = Result(
|
||||
fiber: existing,
|
||||
visitChildren: nextView._visitChildren,
|
||||
parent: partialResult,
|
||||
child: existing.child,
|
||||
alternateChild: existing.alternate?.child,
|
||||
newContent: newContent
|
||||
)
|
||||
partialResult.nextExisting = existing.sibling
|
||||
} else {
|
||||
// Otherwise, create a new fiber for this child.
|
||||
let fiber = Fiber(
|
||||
&nextView,
|
||||
element: partialResult.nextExistingAlternate?.element,
|
||||
parent: partialResult.fiber,
|
||||
elementParent: partialResult.fiber?.element != nil
|
||||
? partialResult.fiber
|
||||
: partialResult.fiber?.elementParent,
|
||||
childIndex: partialResult.childrenCount,
|
||||
reconciler: partialResult.fiber?.reconciler
|
||||
)
|
||||
// If a fiber already exists for an alternate, link them.
|
||||
if let alternate = partialResult.nextExistingAlternate {
|
||||
fiber.alternate = alternate
|
||||
partialResult.nextExistingAlternate = alternate.sibling
|
||||
}
|
||||
resultChild = Result(
|
||||
fiber: fiber,
|
||||
visitChildren: nextView._visitChildren,
|
||||
parent: partialResult,
|
||||
child: nil,
|
||||
alternateChild: fiber.alternate?.child
|
||||
)
|
||||
}
|
||||
// Keep track of the index of the child so the LayoutComputer can propose sizes.
|
||||
partialResult.childrenCount += 1
|
||||
// Get the last child element we've processed, and add the new child as its sibling.
|
||||
if let lastSibling = partialResult.lastSibling {
|
||||
lastSibling.fiber?.sibling = resultChild.fiber
|
||||
lastSibling.sibling = resultChild
|
||||
} else {
|
||||
// Otherwise setup the first child
|
||||
partialResult.fiber?.child = resultChild.fiber
|
||||
partialResult.child = resultChild
|
||||
}
|
||||
partialResult.lastSibling = resultChild
|
||||
}
|
||||
}
|
||||
|
||||
final class ReconcilerVisitor: ViewVisitor {
|
||||
unowned let reconciler: FiberReconciler<Renderer>
|
||||
/// The current, mounted `Fiber`.
|
||||
var currentRoot: Fiber
|
||||
var mutations = [Mutation<Renderer>]()
|
||||
|
||||
init(root: Fiber, reconciler: FiberReconciler<Renderer>) {
|
||||
self.reconciler = reconciler
|
||||
currentRoot = root
|
||||
}
|
||||
|
||||
/// Walk the current tree, recomputing at each step to check for discrepancies.
|
||||
///
|
||||
/// Parent-first depth-first traversal.
|
||||
/// Take this `View` tree for example.
|
||||
/// ```swift
|
||||
/// VStack {
|
||||
/// HStack {
|
||||
/// Text("A")
|
||||
/// Text("B")
|
||||
/// }
|
||||
/// Text("C")
|
||||
/// }
|
||||
/// ```
|
||||
/// Basically, we read it like this:
|
||||
/// 1. `VStack` has children, so we go to it's first child, `HStack`.
|
||||
/// 2. `HStack` has children, so we go further to it's first child, `Text`.
|
||||
/// 3. `Text` has no child, but has a sibling, so we go to that.
|
||||
/// 4. `Text` has no child and no sibling, so we return to the `HStack`.
|
||||
/// 5. We've already read the children, so we look for a sibling, `Text`.
|
||||
/// 6. `Text` has no children and no sibling, so we return to the `VStack.`
|
||||
/// We finish once we've returned to the root element.
|
||||
/// ```
|
||||
/// ┌──────┐
|
||||
/// │VStack│
|
||||
/// └──┬───┘
|
||||
/// ▲ 1 │
|
||||
/// │ └──►┌──────┐
|
||||
/// │ │HStack│
|
||||
/// │ ┌─┴───┬──┘
|
||||
/// │ │ ▲ │ 2
|
||||
/// │ │ │ │ ┌────┐
|
||||
/// │ │ │ └─►│Text├─┐
|
||||
/// 6 │ │ 4 │ └────┘ │
|
||||
/// │ │ │ │ 3
|
||||
/// │ 5 │ │ ┌────┐ │
|
||||
/// │ │ └────┤Text│◄┘
|
||||
/// │ │ └────┘
|
||||
/// │ │
|
||||
/// │ └►┌────┐
|
||||
/// │ │Text│
|
||||
/// └───────┴────┘
|
||||
/// ```
|
||||
func visit<V>(_ view: V) where V: View {
|
||||
let alternateRoot: Fiber?
|
||||
if let alternate = currentRoot.alternate {
|
||||
alternateRoot = alternate
|
||||
} else {
|
||||
alternateRoot = currentRoot.createAndBindAlternate?()
|
||||
}
|
||||
let rootResult = TreeReducer.Result(
|
||||
fiber: alternateRoot, // The alternate is the WIP node.
|
||||
visitChildren: view._visitChildren,
|
||||
parent: nil,
|
||||
child: alternateRoot?.child,
|
||||
alternateChild: currentRoot.child
|
||||
)
|
||||
var node = rootResult
|
||||
|
||||
/// A dictionary keyed by the unique ID of an element, with a value indicating what index
|
||||
/// we are currently at. This ensures we place children in the correct order, even if they are
|
||||
/// at different levels in the `View` tree.
|
||||
var elementIndices = [ObjectIdentifier: Int]()
|
||||
|
||||
/// Compare `node` with its alternate, and add any mutations to the list.
|
||||
func reconcile(_ node: TreeReducer.Result) {
|
||||
if let element = node.fiber?.element,
|
||||
let parent = node.fiber?.elementParent?.element
|
||||
{
|
||||
let key = ObjectIdentifier(parent)
|
||||
let index = elementIndices[key, default: 0]
|
||||
if node.fiber?.alternate == nil { // This didn't exist before (no alternate)
|
||||
mutations.append(.insert(element: element, parent: parent, index: index))
|
||||
} else if node.fiber?.typeInfo?.type != node.fiber?.alternate?.typeInfo?.type,
|
||||
let previous = node.fiber?.alternate?.element
|
||||
{
|
||||
// This is a completely different type of view.
|
||||
mutations.append(.replace(parent: parent, previous: previous, replacement: element))
|
||||
} else if let newContent = node.newContent,
|
||||
newContent != element.content
|
||||
{
|
||||
// This is the same type of view, but its backing data has changed.
|
||||
mutations.append(.update(previous: element, newContent: newContent))
|
||||
}
|
||||
elementIndices[key] = index + 1
|
||||
}
|
||||
}
|
||||
|
||||
// The main reconciler loop.
|
||||
while true {
|
||||
// Perform work on the node.
|
||||
reconcile(node)
|
||||
|
||||
// Compute the children of the node.
|
||||
let reducer = TreeReducer.Visitor(initialResult: node)
|
||||
node.visitChildren(reducer)
|
||||
|
||||
// Setup the alternate if it doesn't exist yet.
|
||||
if node.fiber?.alternate == nil {
|
||||
_ = node.fiber?.createAndBindAlternate?()
|
||||
}
|
||||
|
||||
// Walk all down all the way into the deepest child.
|
||||
if let child = reducer.result.child {
|
||||
node = child
|
||||
continue
|
||||
} else if let alternateChild = node.fiber?.alternate?.child {
|
||||
walk(alternateChild) { node in
|
||||
if let element = node.element,
|
||||
let parent = node.elementParent?.element
|
||||
{
|
||||
// The alternate has a child that no longer exists.
|
||||
// Removals must happen in reverse order, so a child element
|
||||
// is removed before its parent.
|
||||
self.mutations.insert(.remove(element: element, parent: parent), at: 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if reducer.result.child == nil {
|
||||
node.fiber?.child = nil // Make sure we clear the child if there was none
|
||||
}
|
||||
|
||||
// If we've made it back to the root, then exit.
|
||||
if node === rootResult {
|
||||
return
|
||||
}
|
||||
// Now walk back up the tree until we find a sibling.
|
||||
while node.sibling == nil {
|
||||
var alternateSibling = node.fiber?.alternate?.sibling
|
||||
while alternateSibling != nil { // The alternate had siblings that no longer exist.
|
||||
if let element = alternateSibling?.element,
|
||||
let parent = alternateSibling?.elementParent?.element
|
||||
{
|
||||
// Removals happen in reverse order, so a child element is removed before
|
||||
// its parent.
|
||||
mutations.insert(.remove(element: element, parent: parent), at: 0)
|
||||
}
|
||||
alternateSibling = alternateSibling?.sibling
|
||||
}
|
||||
// When we walk back to the root, exit
|
||||
guard let parent = node.parent,
|
||||
parent !== currentRoot.alternate
|
||||
else {
|
||||
return
|
||||
}
|
||||
node = parent
|
||||
}
|
||||
// Walk across to the sibling, and repeat.
|
||||
// swiftlint:disable:next force_unwrap
|
||||
node = node.sibling!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reconcile(from root: Fiber) {
|
||||
// Create a list of mutations.
|
||||
let visitor = ReconcilerVisitor(root: root, reconciler: self)
|
||||
root.visitView(visitor)
|
||||
|
||||
// Apply mutations to the rendered output.
|
||||
renderer.commit(visitor.mutations)
|
||||
|
||||
// Swap the root out for its alternate.
|
||||
// Essentially, making the work in progress tree the current,
|
||||
// and leaving the current available to be the work in progress
|
||||
// on our next update.
|
||||
let child = root.child
|
||||
root.child = root.alternate?.child
|
||||
root.alternate?.child = child
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2021 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.
|
||||
//
|
||||
|
||||
/// A renderer capable of performing mutations specified by a `FiberReconciler`.
|
||||
public protocol FiberRenderer {
|
||||
/// The element class this renderer uses.
|
||||
associatedtype ElementType: FiberElement
|
||||
/// Check whether a `View` is a primitive for this renderer.
|
||||
static func isPrimitive<V>(_ view: V) -> Bool where V: View
|
||||
/// Apply the mutations to the elements.
|
||||
func commit(_ mutations: [Mutation<Self>])
|
||||
/// The root element all top level views should be mounted on.
|
||||
var rootElement: ElementType { get }
|
||||
/// The smallest set of initial `EnvironmentValues` needed for this renderer to function.
|
||||
var defaultEnvironment: EnvironmentValues { get }
|
||||
}
|
||||
|
||||
public extension FiberRenderer {
|
||||
var defaultEnvironment: EnvironmentValues { .init() }
|
||||
|
||||
@discardableResult
|
||||
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
|
||||
.init(self, view)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2021 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/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A type that is able to propose sizes for its children.
|
||||
protocol LayoutComputer {
|
||||
/// Will be called every time a child is evaluated.
|
||||
/// The calls will always be in order, and no more than one call will be made per child.
|
||||
func proposeSize<V: View>(for child: V, at index: Int) -> CGSize
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2021 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.
|
||||
//
|
||||
|
||||
public enum Mutation<Renderer: FiberRenderer> {
|
||||
case insert(
|
||||
element: Renderer.ElementType,
|
||||
parent: Renderer.ElementType,
|
||||
index: Int
|
||||
)
|
||||
case remove(element: Renderer.ElementType, parent: Renderer.ElementType?)
|
||||
case replace(
|
||||
parent: Renderer.ElementType,
|
||||
previous: Renderer.ElementType,
|
||||
replacement: Renderer.ElementType
|
||||
)
|
||||
case update(previous: Renderer.ElementType, newContent: Renderer.ElementType.Content)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2021 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/7/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Data passed to `_makeView` to create the `ViewOutputs` used in reconciling/rendering.
|
||||
public struct ViewInputs<V: View> {
|
||||
let view: V
|
||||
/// The size proposed by this view's parent.
|
||||
let proposedSize: CGSize?
|
||||
let environment: EnvironmentBox
|
||||
}
|
||||
|
||||
/// Data used to reconcile and render a `View` and its children.
|
||||
public struct ViewOutputs {
|
||||
/// A container for the current `EnvironmentValues`.
|
||||
/// This is stored as a reference to avoid copying the environment when unnecessary.
|
||||
let environment: EnvironmentBox
|
||||
let preferences: _PreferenceStore
|
||||
/// The size requested by this view.
|
||||
let size: CGSize
|
||||
/// The `LayoutComputer` used to propose sizes for the children of this view.
|
||||
let layoutComputer: LayoutComputer?
|
||||
}
|
||||
|
||||
final class EnvironmentBox {
|
||||
let environment: EnvironmentValues
|
||||
|
||||
init(_ environment: EnvironmentValues) {
|
||||
self.environment = environment
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewOutputs {
|
||||
init<V: View>(
|
||||
inputs: ViewInputs<V>,
|
||||
environment: EnvironmentValues? = nil,
|
||||
preferences: _PreferenceStore? = nil,
|
||||
size: CGSize? = nil,
|
||||
layoutComputer: LayoutComputer? = nil
|
||||
) {
|
||||
// Only replace the EnvironmentBox when we change the environment. Otherwise the same box can be reused.
|
||||
self.environment = environment.map(EnvironmentBox.init) ?? inputs.environment
|
||||
self.preferences = preferences ?? .init()
|
||||
self.size = size ?? inputs.proposedSize ?? .zero
|
||||
self.layoutComputer = layoutComputer
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
// By default, we simply pass the inputs through without modifications
|
||||
// or layout considerations.
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
.init(inputs: inputs)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ModifiedContent where Content: View, Modifier: ViewModifier {
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
// Update the environment if needed.
|
||||
var environment = inputs.environment.environment
|
||||
if let environmentWriter = inputs.view.modifier as? EnvironmentModifier {
|
||||
environmentWriter.modifyEnvironment(&environment)
|
||||
}
|
||||
return .init(inputs: inputs, environment: environment)
|
||||
}
|
||||
|
||||
func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
// Visit the computed body of the modifier.
|
||||
visitor.visit(modifier.body(content: .init(modifier: modifier, view: content)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2021 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/3/22.
|
||||
//
|
||||
|
||||
public protocol ViewVisitor {
|
||||
func visit<V: View>(_ view: V)
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func _visitChildren<V: ViewVisitor>(_ visitor: V) {
|
||||
visitor.visit(body)
|
||||
}
|
||||
}
|
||||
|
||||
typealias ViewVisitorF<V: ViewVisitor> = (V) -> ()
|
||||
|
||||
protocol ViewReducer {
|
||||
associatedtype Result
|
||||
static func reduce<V: View>(into partialResult: inout Result, nextView: V)
|
||||
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result
|
||||
}
|
||||
|
||||
extension ViewReducer {
|
||||
static func reduce<V: View>(into partialResult: inout Result, nextView: V) {
|
||||
partialResult = Self.reduce(partialResult: partialResult, nextView: nextView)
|
||||
}
|
||||
|
||||
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result {
|
||||
var result = partialResult
|
||||
Self.reduce(into: &result, nextView: nextView)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
final class ReducerVisitor<R: ViewReducer>: ViewVisitor {
|
||||
var result: R.Result
|
||||
|
||||
init(initialResult: R.Result) {
|
||||
result = initialResult
|
||||
}
|
||||
|
||||
func visit<V>(_ view: V) where V: View {
|
||||
R.reduce(into: &result, nextView: view)
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewReducer {
|
||||
typealias Visitor = ReducerVisitor<Self>
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2021 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/11/22.
|
||||
//
|
||||
|
||||
enum WalkWorkResult<Success> {
|
||||
case `continue`
|
||||
case `break`(with: Success)
|
||||
case pause
|
||||
}
|
||||
|
||||
enum WalkResult<Renderer: FiberRenderer, Success> {
|
||||
case success(Success)
|
||||
case finished
|
||||
case paused(at: FiberReconciler<Renderer>.Fiber)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func walk<Renderer: FiberRenderer>(
|
||||
_ root: FiberReconciler<Renderer>.Fiber,
|
||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> Bool
|
||||
) rethrows -> WalkResult<Renderer, ()> {
|
||||
try walk(root) {
|
||||
try work($0) ? .continue : .pause
|
||||
}
|
||||
}
|
||||
|
||||
/// Parent-first depth-first traversal of a `Fiber` tree.
|
||||
func walk<Renderer: FiberRenderer, Success>(
|
||||
_ root: FiberReconciler<Renderer>.Fiber,
|
||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> WalkWorkResult<Success>
|
||||
) rethrows -> WalkResult<Renderer, Success> {
|
||||
var current = root
|
||||
while true {
|
||||
// Perform work on the node
|
||||
switch try work(current) {
|
||||
case .continue: break
|
||||
case let .break(success): return .success(success)
|
||||
case .pause: return .paused(at: current)
|
||||
}
|
||||
// Walk into the child
|
||||
if let child = current.child {
|
||||
current = child
|
||||
continue
|
||||
}
|
||||
// When we walk back to the root, exit
|
||||
if current === root {
|
||||
return .finished
|
||||
}
|
||||
// Walk back up until we find a sibling
|
||||
while current.sibling == nil {
|
||||
// When we walk back to the root, exit
|
||||
guard let parent = current.parent,
|
||||
parent !== root else { return .finished }
|
||||
current = parent
|
||||
}
|
||||
// Walk the sibling
|
||||
// swiftlint:disable:next force_unwrap
|
||||
current = current.sibling!
|
||||
}
|
||||
}
|
|
@ -23,15 +23,27 @@ public struct _ViewModifier_Content<Modifier>: View
|
|||
{
|
||||
public let modifier: Modifier
|
||||
public let view: AnyView
|
||||
let visitChildren: (ViewVisitor) -> ()
|
||||
|
||||
public init(modifier: Modifier, view: AnyView) {
|
||||
self.modifier = modifier
|
||||
self.view = view
|
||||
visitChildren = { $0.visit(view) }
|
||||
}
|
||||
|
||||
public init<V: View>(modifier: Modifier, view: V) {
|
||||
self.modifier = modifier
|
||||
self.view = AnyView(view)
|
||||
visitChildren = { $0.visit(view) }
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
view
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitChildren(visitor)
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
|
|
|
@ -20,7 +20,21 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
public struct PropertyInfo {
|
||||
public struct PropertyInfo: Hashable {
|
||||
// Hashable/Equatable conformance is not synthesize for metatypes.
|
||||
public static func == (lhs: PropertyInfo, rhs: PropertyInfo) -> Bool {
|
||||
lhs.name == rhs.name && lhs.type == rhs.type && lhs.isVar == rhs.isVar && lhs.offset == rhs
|
||||
.offset && lhs.ownerType == rhs.ownerType
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(name)
|
||||
hasher.combine(ObjectIdentifier(type))
|
||||
hasher.combine(isVar)
|
||||
hasher.combine(offset)
|
||||
hasher.combine(ObjectIdentifier(ownerType))
|
||||
}
|
||||
|
||||
public let name: String
|
||||
public let type: Any.Type
|
||||
public let isVar: Bool
|
||||
|
|
|
@ -40,6 +40,8 @@ public struct AnyView: _PrimitiveView {
|
|||
*/
|
||||
let bodyType: Any.Type
|
||||
|
||||
let visitChildren: (ViewVisitor, Any) -> ()
|
||||
|
||||
public init<V>(_ view: V) where V: View {
|
||||
if let anyView = view as? AnyView {
|
||||
self = anyView
|
||||
|
@ -52,8 +54,14 @@ public struct AnyView: _PrimitiveView {
|
|||
self.view = view
|
||||
// swiftlint:disable:next force_cast
|
||||
bodyClosure = { AnyView(($0 as! V).body) }
|
||||
// swiftlint:disable:next force_cast
|
||||
visitChildren = { $0.visit($1 as! V) }
|
||||
}
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitChildren(visitor, view)
|
||||
}
|
||||
}
|
||||
|
||||
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
||||
|
|
|
@ -48,6 +48,12 @@ public struct ForEach<Data, ID, Content>: _PrimitiveView where Data: RandomAcces
|
|||
self.id = id
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
for element in data {
|
||||
visitor.visit(content(element))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ForEach: ForEachProtocol where Data.Index == Int {
|
||||
|
|
|
@ -22,26 +22,46 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
public let value: T
|
||||
|
||||
let _children: [AnyView]
|
||||
private let visit: (ViewVisitor) -> ()
|
||||
|
||||
public init(_ value: T) {
|
||||
self.value = value
|
||||
_children = []
|
||||
visit = { _ in }
|
||||
}
|
||||
|
||||
public init(_ value: T, children: [AnyView]) {
|
||||
self.value = value
|
||||
_children = children
|
||||
visit = {
|
||||
for child in children {
|
||||
$0.visit(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visit(visitor)
|
||||
}
|
||||
|
||||
init<T1: View, T2: View>(_ v1: T1, _ v2: T2) where T == (T1, T2) {
|
||||
value = (v1, v2)
|
||||
_children = [AnyView(v1), AnyView(v2)]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
init<T1: View, T2: View, T3: View>(_ v1: T1, _ v2: T2, _ v3: T3) where T == (T1, T2, T3) {
|
||||
value = (v1, v2, v3)
|
||||
_children = [AnyView(v1), AnyView(v2), AnyView(v3)]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View>(_ v1: T1, _ v2: T2, _ v3: T3, _ v4: T4)
|
||||
|
@ -49,6 +69,12 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
{
|
||||
value = (v1, v2, v3, v4)
|
||||
_children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4)]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View, T5: View>(
|
||||
|
@ -60,6 +86,13 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
) where T == (T1, T2, T3, T4, T5) {
|
||||
value = (v1, v2, v3, v4, v5)
|
||||
_children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4), AnyView(v5)]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View, T5: View, T6: View>(
|
||||
|
@ -72,6 +105,14 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
) where T == (T1, T2, T3, T4, T5, T6) {
|
||||
value = (v1, v2, v3, v4, v5, v6)
|
||||
_children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4), AnyView(v5), AnyView(v6)]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
$0.visit(v6)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View, T5: View, T6: View, T7: View>(
|
||||
|
@ -93,6 +134,15 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
AnyView(v6),
|
||||
AnyView(v7),
|
||||
]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
$0.visit(v6)
|
||||
$0.visit(v7)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View, T5: View, T6: View, T7: View, T8: View>(
|
||||
|
@ -116,6 +166,16 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
AnyView(v7),
|
||||
AnyView(v8),
|
||||
]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
$0.visit(v6)
|
||||
$0.visit(v7)
|
||||
$0.visit(v8)
|
||||
}
|
||||
}
|
||||
|
||||
init<T1: View, T2: View, T3: View, T4: View, T5: View, T6: View, T7: View, T8: View, T9: View>(
|
||||
|
@ -141,6 +201,17 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
AnyView(v8),
|
||||
AnyView(v9),
|
||||
]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
$0.visit(v6)
|
||||
$0.visit(v7)
|
||||
$0.visit(v8)
|
||||
$0.visit(v9)
|
||||
}
|
||||
}
|
||||
|
||||
init<
|
||||
|
@ -179,6 +250,18 @@ public struct TupleView<T>: _PrimitiveView {
|
|||
AnyView(v9),
|
||||
AnyView(v10),
|
||||
]
|
||||
visit = {
|
||||
$0.visit(v1)
|
||||
$0.visit(v2)
|
||||
$0.visit(v3)
|
||||
$0.visit(v4)
|
||||
$0.visit(v5)
|
||||
$0.visit(v6)
|
||||
$0.visit(v7)
|
||||
$0.visit(v8)
|
||||
$0.visit(v9)
|
||||
$0.visit(v10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ public struct Button<Label>: View where Label: View {
|
|||
}
|
||||
}
|
||||
|
||||
public struct _PrimitiveButtonStyleBody<Label>: _PrimitiveView where Label: View {
|
||||
public struct _PrimitiveButtonStyleBody<Label>: View where Label: View {
|
||||
public let label: Label
|
||||
public let role: ButtonRole?
|
||||
public let action: () -> ()
|
||||
|
@ -85,9 +85,17 @@ public struct _PrimitiveButtonStyleBody<Label>: _PrimitiveView where Label: View
|
|||
}
|
||||
|
||||
@Environment(\.controlSize) public var controlSize
|
||||
|
||||
public var body: Never {
|
||||
neverBody("_PrimitiveButtonStyleBody")
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(label)
|
||||
}
|
||||
}
|
||||
|
||||
public struct _Button<Label>: _PrimitiveView where Label: View {
|
||||
public struct _Button<Label>: View where Label: View {
|
||||
public let label: Label
|
||||
public let role: ButtonRole?
|
||||
public let action: () -> ()
|
||||
|
@ -104,6 +112,10 @@ public struct _Button<Label>: _PrimitiveView where Label: View {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
makeStyleBody()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Button where Label == Text {
|
||||
|
|
|
@ -32,7 +32,7 @@ public let defaultStackSpacing: CGFloat = 8
|
|||
/// Text("Hello")
|
||||
/// Text("World")
|
||||
/// }
|
||||
public struct HStack<Content>: _PrimitiveView where Content: View {
|
||||
public struct HStack<Content>: View where Content: View {
|
||||
public let alignment: VerticalAlignment
|
||||
let spacing: CGFloat
|
||||
public let content: Content
|
||||
|
@ -46,6 +46,14 @@ public struct HStack<Content>: _PrimitiveView where Content: View {
|
|||
self.spacing = spacing ?? defaultStackSpacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: Never {
|
||||
neverBody("HStack")
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
||||
extension HStack: ParentView {
|
||||
|
|
|
@ -27,7 +27,7 @@ public enum HorizontalAlignment: Equatable {
|
|||
/// Text("Hello")
|
||||
/// Text("World")
|
||||
/// }
|
||||
public struct VStack<Content>: _PrimitiveView where Content: View {
|
||||
public struct VStack<Content>: View where Content: View {
|
||||
public let alignment: HorizontalAlignment
|
||||
let spacing: CGFloat
|
||||
public let content: Content
|
||||
|
@ -41,6 +41,14 @@ public struct VStack<Content>: _PrimitiveView where Content: View {
|
|||
self.spacing = spacing ?? defaultStackSpacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: Never {
|
||||
neverBody("VStack")
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
||||
extension VStack: ParentView {
|
||||
|
|
|
@ -19,6 +19,9 @@ public protocol View {
|
|||
associatedtype Body: View
|
||||
|
||||
@ViewBuilder var body: Self.Body { get }
|
||||
|
||||
func _visitChildren<V: ViewVisitor>(_ visitor: V)
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
|
||||
}
|
||||
|
||||
public extension Never {
|
||||
|
@ -38,6 +41,8 @@ public extension _PrimitiveView {
|
|||
var body: Never {
|
||||
neverBody(String(reflecting: Self.self))
|
||||
}
|
||||
|
||||
func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {}
|
||||
}
|
||||
|
||||
/// A `View` type that renders with subviews, usually specified in the `Content` type argument
|
||||
|
|
|
@ -31,6 +31,15 @@ public struct _ConditionalContent<TrueContent, FalseContent>: _PrimitiveView
|
|||
}
|
||||
|
||||
let storage: Storage
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
switch storage {
|
||||
case let .trueContent(view):
|
||||
visitor.visit(view)
|
||||
case let .falseContent(view):
|
||||
visitor.visit(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _ConditionalContent: GroupView {
|
||||
|
@ -52,6 +61,15 @@ extension Optional: View where Wrapped: View {
|
|||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
switch self {
|
||||
case .none:
|
||||
break
|
||||
case let .some(wrapped):
|
||||
visitor.visit(wrapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol AnyOptional {
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
import Benchmark
|
||||
import TokamakCore
|
||||
@_spi(TokamakCore) import TokamakCore
|
||||
import TokamakTestRenderer
|
||||
|
||||
private let bigType = NavigationView<HStack<VStack<Button<Text>>>>.self
|
||||
|
||||
|
@ -25,4 +26,233 @@ benchmark("typeConstructorName TokamakCore") {
|
|||
_ = typeConstructorName(bigType)
|
||||
}
|
||||
|
||||
struct UpdateWide: View {
|
||||
@State var update = -1
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(0..<1000) {
|
||||
if update == $0 {
|
||||
Text("Updated")
|
||||
} else {
|
||||
Text("\($0)")
|
||||
}
|
||||
}
|
||||
Button("Update") {
|
||||
update = 999
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update wide (StackReconciler)") { state in
|
||||
let view = UpdateWide()
|
||||
let renderer = TestRenderer(view)
|
||||
var button: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>?
|
||||
mapAnyView(
|
||||
renderer.rootTarget.subviews[0].subviews[1].subviews[0]
|
||||
.view
|
||||
) { (v: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>) in
|
||||
button = v
|
||||
}
|
||||
try state.measure {
|
||||
button?.action()
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update wide (FiberReconciler)") { state in
|
||||
let view = UpdateWide()
|
||||
let reconciler = TestFiberRenderer(.root).render(view)
|
||||
let button = reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // UpdateLast
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child?.sibling? // Button
|
||||
.child? // ConditionalContent
|
||||
.child? // AnyView
|
||||
.child? // _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
|
||||
.view
|
||||
try state.measure {
|
||||
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.action()
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateNarrow: View {
|
||||
@State var update = -1
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ForEach(0..<1000) {
|
||||
if update == $0 {
|
||||
Text("Updated")
|
||||
} else {
|
||||
Text("\($0)")
|
||||
}
|
||||
}
|
||||
Button("Update") {
|
||||
update = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update narrow (StackReconciler)") { state in
|
||||
let view = UpdateNarrow()
|
||||
let renderer = TestRenderer(view)
|
||||
var button: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>?
|
||||
mapAnyView(
|
||||
renderer.rootTarget.subviews[0].subviews[1].subviews[0]
|
||||
.view
|
||||
) { (v: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>) in
|
||||
button = v
|
||||
}
|
||||
try state.measure {
|
||||
button?.action()
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update narrow (FiberReconciler)") { state in
|
||||
let view = UpdateNarrow()
|
||||
let reconciler = TestFiberRenderer(.root).render(view)
|
||||
let button = reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // UpdateLast
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child?.sibling? // Button
|
||||
.child? // ConditionalContent
|
||||
.child? // AnyView
|
||||
.child? // _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
|
||||
.view
|
||||
try state.measure {
|
||||
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.action()
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateDeep: View {
|
||||
@State var update = "A"
|
||||
|
||||
struct RecursiveView: View {
|
||||
let count: Int
|
||||
let content: String
|
||||
|
||||
init(_ count: Int, content: String) {
|
||||
self.count = count
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if count == 0 {
|
||||
Text(content)
|
||||
} else {
|
||||
RecursiveView(count - 1, content: content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
RecursiveView(1000, content: update)
|
||||
Button("Update") {
|
||||
update = "B"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update deep (StackReconciler)") { state in
|
||||
let view = UpdateDeep()
|
||||
let renderer = TestRenderer(view)
|
||||
var button: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>?
|
||||
mapAnyView(
|
||||
renderer.rootTarget.subviews[0].subviews[1].subviews[0]
|
||||
.view
|
||||
) { (v: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>) in
|
||||
button = v
|
||||
}
|
||||
try state.measure {
|
||||
button?.action()
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update deep (FiberReconciler)") { state in
|
||||
let view = UpdateDeep()
|
||||
let reconciler = TestFiberRenderer(.root).render(view)
|
||||
let button = reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // UpdateLast
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child?.sibling? // Button
|
||||
.child? // ConditionalContent
|
||||
.child? // AnyView
|
||||
.child? // _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
|
||||
.view
|
||||
try state.measure {
|
||||
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.action()
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateShallow: View {
|
||||
@State var update = "A"
|
||||
|
||||
struct RecursiveView: View {
|
||||
let count: Int
|
||||
|
||||
init(_ count: Int) {
|
||||
self.count = count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if count == 0 {
|
||||
Text("RecursiveView")
|
||||
} else {
|
||||
RecursiveView(count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(update)
|
||||
RecursiveView(500)
|
||||
Button("Update") {
|
||||
update = "B"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("update shallow (StackReconciler)") { _ in
|
||||
let view = UpdateShallow()
|
||||
let renderer = TestRenderer(view)
|
||||
var button: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>?
|
||||
mapAnyView(
|
||||
renderer.rootTarget.subviews[0].subviews[1].subviews[0]
|
||||
.view
|
||||
) { (v: _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>) in
|
||||
button = v
|
||||
}
|
||||
// Using state.measure here hangs the benchmark app?
|
||||
button?.action()
|
||||
}
|
||||
|
||||
benchmark("update shallow (FiberReconciler)") { _ in
|
||||
let view = UpdateShallow()
|
||||
let reconciler = TestFiberRenderer(.root).render(view)
|
||||
let button = reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // UpdateLast
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child?.sibling? // Button
|
||||
.child? // ConditionalContent
|
||||
.child? // AnyView
|
||||
.child? // _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
|
||||
.view
|
||||
// Using state.measure here hangs the benchmark app?g
|
||||
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.action()
|
||||
}
|
||||
|
||||
Benchmark.main()
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2021 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/7/22.
|
||||
//
|
||||
|
||||
import JavaScriptKit
|
||||
@_spi(TokamakCore) import TokamakCore
|
||||
@_spi(TokamakStaticHTML) import TokamakStaticHTML
|
||||
|
||||
public final class DOMElement: FiberElement {
|
||||
var reference: JSObject?
|
||||
|
||||
public struct Content: FiberElementContent {
|
||||
let tag: String
|
||||
let attributes: [HTMLAttribute: String]
|
||||
let innerHTML: String?
|
||||
let listeners: [String: Listener]
|
||||
let debugData: [String: ConvertibleToJSValue]
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.tag == rhs.tag && lhs.attributes == rhs.attributes && lhs.innerHTML == rhs.innerHTML
|
||||
}
|
||||
}
|
||||
|
||||
public var content: Content
|
||||
|
||||
public init(from content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public func update(with content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
public extension DOMElement.Content {
|
||||
init<V>(from primitiveView: V) where V: View {
|
||||
guard let primitiveView = primitiveView as? HTMLConvertible else { fatalError() }
|
||||
tag = primitiveView.tag
|
||||
attributes = primitiveView.attributes
|
||||
innerHTML = primitiveView.innerHTML
|
||||
|
||||
if let primitiveView = primitiveView as? DOMNodeConvertible {
|
||||
listeners = primitiveView.listeners
|
||||
} else {
|
||||
listeners = [:]
|
||||
}
|
||||
|
||||
debugData = [
|
||||
"view": String(reflecting: V.self),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protocol DOMNodeConvertible: HTMLConvertible {
|
||||
var listeners: [String: Listener] { get }
|
||||
}
|
||||
|
||||
public struct DOMFiberRenderer: FiberRenderer {
|
||||
public let rootElement: DOMElement
|
||||
|
||||
public var defaultEnvironment: EnvironmentValues {
|
||||
var environment = EnvironmentValues()
|
||||
environment[_ColorSchemeKey.self] = .light
|
||||
return environment
|
||||
}
|
||||
|
||||
public init(_ rootSelector: String) {
|
||||
guard let reference = document.querySelector!(rootSelector).object else {
|
||||
fatalError("""
|
||||
The root element with selector '\(rootSelector)' could not be found. \
|
||||
Ensure this element exists in your site's index.html file.
|
||||
""")
|
||||
}
|
||||
rootElement = .init(
|
||||
from: .init(
|
||||
tag: "",
|
||||
attributes: [:],
|
||||
innerHTML: nil,
|
||||
listeners: [:],
|
||||
debugData: ["view": "root"]
|
||||
)
|
||||
)
|
||||
rootElement.reference = reference
|
||||
}
|
||||
|
||||
public static func isPrimitive<V>(_ view: V) -> Bool where V: View {
|
||||
view is HTMLConvertible || view is DOMNodeConvertible
|
||||
}
|
||||
|
||||
private func createElement(_ element: DOMElement) -> JSObject {
|
||||
let result = document.createElement!(element.content.tag).object!
|
||||
apply(element.content, to: result)
|
||||
element.reference = result
|
||||
return result
|
||||
}
|
||||
|
||||
private func apply(_ content: DOMElement.Content, to element: JSObject) {
|
||||
for (attribute, value) in content.attributes {
|
||||
if attribute.isUpdatedAsProperty {
|
||||
element[attribute.value] = .string(value)
|
||||
} else {
|
||||
_ = element.setAttribute?(attribute.value, value)
|
||||
}
|
||||
}
|
||||
if let innerHTML = content.innerHTML {
|
||||
element.innerHTML = .string(innerHTML)
|
||||
}
|
||||
for (event, action) in content.listeners {
|
||||
_ = element.addEventListener?(event, JSClosure {
|
||||
action($0[0].object!)
|
||||
return .undefined
|
||||
})
|
||||
}
|
||||
for (key, value) in content.debugData {
|
||||
element.dataset.object?[dynamicMember: key] = value.jsValue
|
||||
}
|
||||
}
|
||||
|
||||
public func commit(_ mutations: [Mutation<Self>]) {
|
||||
for mutation in mutations {
|
||||
switch mutation {
|
||||
case let .insert(newElement, parent, index):
|
||||
let element = createElement(newElement)
|
||||
guard let parentElement = parent.reference ?? rootElement.reference
|
||||
else { fatalError("The root element was not bound (trying to insert element).") }
|
||||
if Int(parentElement.children.object?.length.number ?? 0) > index {
|
||||
_ = parentElement.insertBefore?(element, parentElement.children[index])
|
||||
} else {
|
||||
_ = parentElement.appendChild?(element)
|
||||
}
|
||||
case let .remove(element, _):
|
||||
_ = element.reference?.remove?()
|
||||
case let .replace(parent, previous, replacement):
|
||||
guard let parentElement = parent.reference ?? rootElement.reference
|
||||
else { fatalError("The root element was not bound (trying to replace element).") }
|
||||
guard let previousElement = previous.reference else {
|
||||
fatalError("The previous element does not exist (trying to replace element).")
|
||||
}
|
||||
let replacementElement = createElement(replacement)
|
||||
_ = parentElement.replaceChild?(previousElement, replacementElement)
|
||||
case let .update(previous, newContent):
|
||||
previous.update(with: newContent)
|
||||
guard let previousElement = previous.reference
|
||||
else { fatalError("The element does not exist (trying to update element).") }
|
||||
apply(newContent, to: previousElement)
|
||||
previous.reference = previousElement
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _PrimitiveButtonStyleBody: DOMNodeConvertible {
|
||||
public var tag: String { "button" }
|
||||
public var attributes: [HTMLAttribute: String] { [:] }
|
||||
var listeners: [String: Listener] {
|
||||
["pointerup": { _ in self.action() }]
|
||||
}
|
||||
}
|
|
@ -88,7 +88,9 @@ final class DOMRenderer: Renderer {
|
|||
rootRef = ref
|
||||
appendRootStyle(ref)
|
||||
|
||||
JavaScriptEventLoop.installGlobalExecutor()
|
||||
if #available(macOS 10.15, *) {
|
||||
JavaScriptEventLoop.installGlobalExecutor()
|
||||
}
|
||||
|
||||
let scheduler = JSScheduler()
|
||||
self.scheduler = scheduler
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
// Copyright 2021 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/6/22.
|
||||
//
|
||||
|
||||
import TokamakCore
|
||||
|
||||
public final class HTMLElement: FiberElement, CustomStringConvertible {
|
||||
public struct Content: FiberElementContent, Equatable {
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.tag == rhs.tag
|
||||
&& lhs.attributes == rhs.attributes
|
||||
&& lhs.innerHTML == rhs.innerHTML
|
||||
&& lhs.children.map(\.content) == rhs.children.map(\.content)
|
||||
}
|
||||
|
||||
var tag: String
|
||||
var attributes: [HTMLAttribute: String]
|
||||
var innerHTML: String?
|
||||
var children: [HTMLElement] = []
|
||||
|
||||
public init<V>(from primitiveView: V) where V: View {
|
||||
guard let primitiveView = primitiveView as? HTMLConvertible else { fatalError() }
|
||||
tag = primitiveView.tag
|
||||
attributes = primitiveView.attributes
|
||||
innerHTML = primitiveView.innerHTML
|
||||
}
|
||||
|
||||
public init(
|
||||
tag: String,
|
||||
attributes: [HTMLAttribute: String],
|
||||
innerHTML: String?,
|
||||
children: [HTMLElement]
|
||||
) {
|
||||
self.tag = tag
|
||||
self.attributes = attributes
|
||||
self.innerHTML = innerHTML
|
||||
self.children = children
|
||||
}
|
||||
}
|
||||
|
||||
public var content: Content
|
||||
|
||||
public init(from content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public init(
|
||||
tag: String,
|
||||
attributes: [HTMLAttribute: String],
|
||||
innerHTML: String?,
|
||||
children: [HTMLElement]
|
||||
) {
|
||||
content = .init(
|
||||
tag: tag,
|
||||
attributes: attributes,
|
||||
innerHTML: innerHTML,
|
||||
children: children
|
||||
)
|
||||
}
|
||||
|
||||
public func update(with content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"""
|
||||
<\(content.tag)\(content.attributes.map { " \($0.key.value)=\"\($0.value)\"" }
|
||||
.joined(separator: ""))>\(content.innerHTML != nil ? "\(content.innerHTML!)" : "")\(!content
|
||||
.children
|
||||
.isEmpty ? "\n" : "")\(content.children.map(\.description).joined(separator: "\n"))\(!content
|
||||
.children
|
||||
.isEmpty ? "\n" : "")</\(content.tag)>
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML) public protocol HTMLConvertible {
|
||||
var tag: String { get }
|
||||
var attributes: [HTMLAttribute: String] { get }
|
||||
var innerHTML: String? { get }
|
||||
}
|
||||
|
||||
public extension HTMLConvertible {
|
||||
@_spi(TokamakStaticHTML) var innerHTML: String? { nil }
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML) extension Text: HTMLConvertible {
|
||||
@_spi(TokamakStaticHTML) public var innerHTML: String? {
|
||||
_TextProxy(self).rawText
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML) extension VStack: HTMLConvertible {
|
||||
@_spi(TokamakStaticHTML) public var tag: String { "div" }
|
||||
@_spi(TokamakStaticHTML) public var attributes: [HTMLAttribute: String] {
|
||||
let spacing = _VStackProxy(self).spacing
|
||||
return [
|
||||
"style": """
|
||||
justify-items: \(alignment.cssValue);
|
||||
\(hasSpacer ? "height: 100%;" : "")
|
||||
\(fillCrossAxis ? "width: 100%;" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px;" : "")
|
||||
""",
|
||||
"class": "_tokamak-stack _tokamak-vstack",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakStaticHTML) extension HStack: HTMLConvertible {
|
||||
@_spi(TokamakStaticHTML) public var tag: String { "div" }
|
||||
@_spi(TokamakStaticHTML) public var attributes: [HTMLAttribute: String] {
|
||||
let spacing = _HStackProxy(self).spacing
|
||||
return [
|
||||
"style": """
|
||||
align-items: \(alignment.cssValue);
|
||||
\(hasSpacer ? "width: 100%;" : "")
|
||||
\(fillCrossAxis ? "height: 100%;" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px;" : "")
|
||||
""",
|
||||
"class": "_tokamak-stack _tokamak-hstack",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public struct StaticHTMLFiberRenderer: FiberRenderer {
|
||||
public let rootElement: HTMLElement
|
||||
public let defaultEnvironment: EnvironmentValues
|
||||
|
||||
public init() {
|
||||
rootElement = .init(tag: "body", attributes: [:], innerHTML: nil, children: [])
|
||||
var environment = EnvironmentValues()
|
||||
environment[_ColorSchemeKey.self] = .light
|
||||
defaultEnvironment = environment
|
||||
}
|
||||
|
||||
public static func isPrimitive<V>(_ view: V) -> Bool where V: View {
|
||||
view is HTMLConvertible
|
||||
}
|
||||
|
||||
public func commit(_ mutations: [Mutation<Self>]) {
|
||||
for mutation in mutations {
|
||||
switch mutation {
|
||||
case let .insert(element, parent, index):
|
||||
parent.content.children.insert(element, at: index)
|
||||
case let .remove(element, parent):
|
||||
parent?.content.children.removeAll(where: { $0 === element })
|
||||
case let .replace(parent, previous, replacement):
|
||||
guard let index = parent.content.children.firstIndex(where: { $0 === previous })
|
||||
else { continue }
|
||||
parent.content.children[index] = replacement
|
||||
case let .update(previous, newContent):
|
||||
previous.update(with: newContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,4 +43,52 @@ benchmark("render List sorted attributes") {
|
|||
_ = StaticHTMLRenderer(List(1..<100) { Text("\($0)") }).render(shouldSortAttributes: true)
|
||||
}
|
||||
|
||||
benchmark("render Text (StackReconciler)") {
|
||||
_ = StaticHTMLRenderer(Text("Hello, world!")).render(shouldSortAttributes: false)
|
||||
}
|
||||
|
||||
benchmark("render Text (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(Text("Hello, world!"))
|
||||
}
|
||||
|
||||
benchmark("render ForEach(100) (StackReconciler)") {
|
||||
_ = StaticHTMLRenderer(ForEach(1..<100) { Text("\($0)") }).render(shouldSortAttributes: false)
|
||||
}
|
||||
|
||||
benchmark("render ForEach(100) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(ForEach(1..<100) { Text("\($0)") })
|
||||
}
|
||||
|
||||
benchmark("render ForEach(1000) (StackReconciler)") {
|
||||
_ = StaticHTMLRenderer(ForEach(1..<1000) { Text("\($0)") }).render(shouldSortAttributes: false)
|
||||
}
|
||||
|
||||
benchmark("render ForEach(1000) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(ForEach(1..<1000) { Text("\($0)") })
|
||||
}
|
||||
|
||||
struct RecursiveView: View {
|
||||
let count: Int
|
||||
|
||||
init(_ count: Int) {
|
||||
self.count = count
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if count == 0 {
|
||||
Text("RecursiveView")
|
||||
} else {
|
||||
RecursiveView(count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmark("render RecursiveView(1000) (StackReconciler)") {
|
||||
_ = StaticHTMLRenderer(RecursiveView(1000)).render(shouldSortAttributes: false)
|
||||
}
|
||||
|
||||
benchmark("render RecursiveView(1000) (FiberReconciler)") {
|
||||
StaticHTMLFiberRenderer().render(RecursiveView(1000))
|
||||
}
|
||||
|
||||
Benchmark.main()
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright 2021 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 4/5/22.
|
||||
//
|
||||
|
||||
@_spi(TokamakCore) import TokamakCore
|
||||
|
||||
public protocol TestFiberPrimitive {
|
||||
var tag: String { get }
|
||||
var attributes: [String: Any] { get }
|
||||
}
|
||||
|
||||
public extension TestFiberPrimitive {
|
||||
var tag: String { String(String(reflecting: Self.self).split(separator: "<")[0]) }
|
||||
}
|
||||
|
||||
extension VStack: TestFiberPrimitive {
|
||||
public var attributes: [String: Any] {
|
||||
["spacing": _VStackProxy(self).spacing, "alignment": alignment]
|
||||
}
|
||||
}
|
||||
|
||||
extension HStack: TestFiberPrimitive {
|
||||
public var attributes: [String: Any] {
|
||||
["spacing": _HStackProxy(self).spacing, "alignment": alignment]
|
||||
}
|
||||
}
|
||||
|
||||
extension Text: TestFiberPrimitive {
|
||||
public var attributes: [String: Any] {
|
||||
let proxy = _TextProxy(self)
|
||||
return ["content": proxy.storage.rawText, "modifiers": proxy.modifiers]
|
||||
}
|
||||
}
|
||||
|
||||
extension _Button: TestFiberPrimitive {
|
||||
public var attributes: [String: Any] {
|
||||
["action": action, "role": role as Any]
|
||||
}
|
||||
}
|
||||
|
||||
extension _PrimitiveButtonStyleBody: TestFiberPrimitive {
|
||||
public var attributes: [String: Any] {
|
||||
["size": controlSize, "role": role as Any]
|
||||
}
|
||||
}
|
||||
|
||||
public final class TestFiberElement: FiberElement, CustomStringConvertible {
|
||||
public struct Content: FiberElementContent, Equatable {
|
||||
let renderedValue: String
|
||||
let closingTag: String
|
||||
|
||||
init(
|
||||
renderedValue: String,
|
||||
closingTag: String
|
||||
) {
|
||||
self.renderedValue = renderedValue
|
||||
self.closingTag = closingTag
|
||||
}
|
||||
|
||||
public init<V>(from primitiveView: V) where V: View {
|
||||
guard let primitiveView = primitiveView as? TestFiberPrimitive else { fatalError() }
|
||||
renderedValue =
|
||||
"<\(primitiveView.tag) \(primitiveView.attributes.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\"\(String(describing: $0.value))\"" }.joined(separator: " "))>"
|
||||
closingTag = "</\(primitiveView.tag)>"
|
||||
}
|
||||
}
|
||||
|
||||
public var content: Content
|
||||
|
||||
public init(from content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"\(content.renderedValue)\(content.closingTag)"
|
||||
}
|
||||
|
||||
public init(renderedValue: String, closingTag: String) {
|
||||
content = .init(renderedValue: renderedValue, closingTag: closingTag)
|
||||
}
|
||||
|
||||
public func update(with content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public static var root: Self { .init(renderedValue: "<root>", closingTag: "</root>") }
|
||||
}
|
||||
|
||||
public struct TestFiberRenderer: FiberRenderer {
|
||||
public typealias ElementType = TestFiberElement
|
||||
|
||||
public let rootElement: ElementType
|
||||
|
||||
public init(_ rootElement: ElementType) {
|
||||
self.rootElement = rootElement
|
||||
}
|
||||
|
||||
public static func isPrimitive<V>(_ view: V) -> Bool where V: View {
|
||||
view is TestFiberPrimitive
|
||||
}
|
||||
|
||||
public func commit(_ mutations: [Mutation<Self>]) {
|
||||
for mutation in mutations {
|
||||
switch mutation {
|
||||
case .insert, .remove, .replace:
|
||||
break
|
||||
case let .update(previous, newContent):
|
||||
previous.update(with: newContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2021 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/3/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@_spi(TokamakCore) @testable import TokamakCore
|
||||
import TokamakTestRenderer
|
||||
|
||||
private struct TestView: View {
|
||||
struct Counter: View {
|
||||
@State private var count = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("\(count)")
|
||||
HStack {
|
||||
if count > 0 {
|
||||
Button("Decrement") {
|
||||
print("Decrement")
|
||||
count -= 1
|
||||
}
|
||||
}
|
||||
if count < 5 {
|
||||
Button("Increment") {
|
||||
print("Increment")
|
||||
if count + 1 >= 5 {
|
||||
print("Hit 5")
|
||||
}
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Counter()
|
||||
}
|
||||
}
|
||||
|
||||
final class VisitorTests: XCTestCase {
|
||||
func testRenderer() {
|
||||
let reconciler = TestFiberRenderer(.root).render(TestView())
|
||||
func decrement() {
|
||||
(
|
||||
reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // TestView
|
||||
.child? // Counter
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child?.sibling? // HStack
|
||||
.child? // TupleView
|
||||
.child? // Optional
|
||||
.child? // Button
|
||||
.view as? Button<Text>
|
||||
)?
|
||||
.action()
|
||||
}
|
||||
func increment() {
|
||||
(
|
||||
reconciler.current // ModifiedContent
|
||||
.child? // _ViewModifier_Content
|
||||
.child? // TestView
|
||||
.child? // Counter
|
||||
.child? // VStack
|
||||
.child? // TupleView
|
||||
.child? // Text
|
||||
.sibling? // HStack
|
||||
.child? // TupleView
|
||||
.child? // Optional
|
||||
.sibling? // Optional
|
||||
.child? // Button
|
||||
.view as? Button<Text>
|
||||
)?
|
||||
.action()
|
||||
}
|
||||
for _ in 0..<5 {
|
||||
increment()
|
||||
}
|
||||
decrement()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue