diff --git a/README.md b/README.md index b62201bf7..f462c042a 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,31 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt localized date formatting (or any arbitrary style/script/font added that way) are available in your app. +### Fiber renderers + +A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber) +is optionally available. It can provide faster updates and allow for larger View hierarchies. +It also includes layout steps that can match SwiftUI layouts closer than CSS approximations. + +You can specify which reconciler to use in your `App`'s configuration: + +```swift +struct CounterApp: App { + static let _configuration: _AppConfiguration = .init( + // Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations. + reconciler: .fiber(useDynamicLayout: true) + ) + + var body: some Scene { + WindowGroup("Counter Demo") { + Counter(count: 5, limit: 15) + } + } +} +``` + +> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet. + ## Requirements ### For app developers diff --git a/Sources/TokamakCore/App/App.swift b/Sources/TokamakCore/App/App.swift index 7f0104549..49f299d14 100644 --- a/Sources/TokamakCore/App/App.swift +++ b/Sources/TokamakCore/App/App.swift @@ -28,7 +28,10 @@ public protocol App: _TitledApp { var body: Body { get } /// Implemented by the renderer to mount the `App` - static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) + static func _launch( + _ app: Self, + with configuration: _AppConfiguration + ) /// Implemented by the renderer to update the `App` on `ScenePhase` changes var _phasePublisher: AnyPublisher { get } @@ -36,14 +39,38 @@ public protocol App: _TitledApp { /// Implemented by the renderer to update the `App` on `ColorScheme` changes var _colorSchemePublisher: AnyPublisher { get } + static var _configuration: _AppConfiguration { get } + static func main() init() } +public struct _AppConfiguration { + public let reconciler: Reconciler + public let rootEnvironment: EnvironmentValues + + public init( + reconciler: Reconciler = .stack, + rootEnvironment: EnvironmentValues = .init() + ) { + self.reconciler = reconciler + self.rootEnvironment = rootEnvironment + } + + public enum Reconciler { + /// Use the `StackReconciler`. + case stack + /// Use the `FiberReconciler` with layout steps optionally enabled. + case fiber(useDynamicLayout: Bool = false) + } +} + public extension App { + static var _configuration: _AppConfiguration { .init() } + static func main() { let app = Self() - _launch(app, EnvironmentValues()) + _launch(app, with: Self._configuration) } } diff --git a/Sources/TokamakCore/App/Scenes/Scene.swift b/Sources/TokamakCore/App/Scenes/Scene.swift index 585fc30c4..65c3122d6 100644 --- a/Sources/TokamakCore/App/Scenes/Scene.swift +++ b/Sources/TokamakCore/App/Scenes/Scene.swift @@ -21,8 +21,23 @@ public protocol Scene { // FIXME: If I put `@SceneBuilder` in front of this // it fails to build with no useful error message. var body: Self.Body { get } + + /// Override the default implementation for `Scene`s with body types of `Never` + /// or in cases where the body would normally need to be type erased. + /// + /// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor` + func _visitChildren(_ visitor: V) + + /// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom + /// `LayoutComputer` from the `SceneInputs`. + /// + /// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`. + static func _makeScene(_ inputs: SceneInputs) -> SceneOutputs } +public typealias SceneInputs = ViewInputs +public typealias SceneOutputs = ViewOutputs + protocol TitledScene { var title: Text? { get } } diff --git a/Sources/TokamakCore/App/Scenes/SceneBuilder.swift b/Sources/TokamakCore/App/Scenes/SceneBuilder.swift index 6c0fe0bcd..09c890879 100644 --- a/Sources/TokamakCore/App/Scenes/SceneBuilder.swift +++ b/Sources/TokamakCore/App/Scenes/SceneBuilder.swift @@ -29,7 +29,14 @@ public extension SceneBuilder { static func buildBlock(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene, C1: Scene { - _TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)]) + _TupleScene( + (c0, c1), + children: [_AnyScene(c0), _AnyScene(c1)], + visit: { + $0.visit(c0) + $0.visit(c1) + } + ) } } @@ -37,7 +44,15 @@ public extension SceneBuilder { static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene where C0: Scene, C1: Scene, C2: Scene { - _TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)]) + _TupleScene( + (c0, c1, c2), + children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + } + ) } } @@ -50,7 +65,13 @@ public extension SceneBuilder { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene { _TupleScene( (c0, c1, c2, c3), - children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)] + children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + } ) } } @@ -65,7 +86,14 @@ public extension SceneBuilder { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene { _TupleScene( (c0, c1, c2, c3, c4), - children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)] + children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + } ) } } @@ -90,7 +118,15 @@ public extension SceneBuilder { _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), - ] + ], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + $0.visit(c5) + } ) } } @@ -117,7 +153,16 @@ public extension SceneBuilder { _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), - ] + ], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + $0.visit(c5) + $0.visit(c6) + } ) } } @@ -146,7 +191,17 @@ public extension SceneBuilder { _AnyScene(c5), _AnyScene(c6), _AnyScene(c7), - ] + ], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + $0.visit(c5) + $0.visit(c6) + $0.visit(c7) + } ) } } @@ -177,7 +232,18 @@ public extension SceneBuilder { _AnyScene(c6), _AnyScene(c7), _AnyScene(c8), - ] + ], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + $0.visit(c5) + $0.visit(c6) + $0.visit(c7) + $0.visit(c8) + } ) } } @@ -210,7 +276,19 @@ public extension SceneBuilder { _AnyScene(c7), _AnyScene(c8), _AnyScene(c9), - ] + ], + visit: { + $0.visit(c0) + $0.visit(c1) + $0.visit(c2) + $0.visit(c3) + $0.visit(c4) + $0.visit(c5) + $0.visit(c6) + $0.visit(c7) + $0.visit(c8) + $0.visit(c9) + } ) } } diff --git a/Sources/TokamakCore/App/Scenes/WindowGroup.swift b/Sources/TokamakCore/App/Scenes/WindowGroup.swift index cd2d5c12a..44751717d 100644 --- a/Sources/TokamakCore/App/Scenes/WindowGroup.swift +++ b/Sources/TokamakCore/App/Scenes/WindowGroup.swift @@ -75,4 +75,8 @@ public struct WindowGroup: Scene, TitledScene where Content: View { // public init(_ titleKey: LocalizedStringKey, // @ViewBuilder content: () -> Content) { // } + + public func _visitChildren(_ visitor: V) where V: SceneVisitor { + visitor.visit(content) + } } diff --git a/Sources/TokamakCore/App/_AnyApp.swift b/Sources/TokamakCore/App/_AnyApp.swift index 650f4ceb8..bea32ea72 100644 --- a/Sources/TokamakCore/App/_AnyApp.swift +++ b/Sources/TokamakCore/App/_AnyApp.swift @@ -42,7 +42,7 @@ public struct _AnyApp: App { } @_spi(TokamakCore) - public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { + public static func _launch(_ app: Self, with configuration: _AppConfiguration) { fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.") } @@ -51,6 +51,10 @@ public struct _AnyApp: App { fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.") } + public static var _configuration: _AppConfiguration { + fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.") + } + @_spi(TokamakCore) public var _phasePublisher: AnyPublisher { fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.") diff --git a/Sources/TokamakCore/App/_TupleScene.swift b/Sources/TokamakCore/App/_TupleScene.swift index 2c2543b5c..8c58b08dd 100644 --- a/Sources/TokamakCore/App/_TupleScene.swift +++ b/Sources/TokamakCore/App/_TupleScene.swift @@ -17,11 +17,17 @@ struct _TupleScene: Scene, GroupScene { let value: T - var children: [_AnyScene] + let children: [_AnyScene] + let visit: (SceneVisitor) -> () - init(_ value: T, children: [_AnyScene]) { + init( + _ value: T, + children: [_AnyScene], + visit: @escaping (SceneVisitor) -> () + ) { self.value = value self.children = children + self.visit = visit } var body: Never { diff --git a/Sources/TokamakCore/Fiber/App/AppVisitor.swift b/Sources/TokamakCore/Fiber/App/AppVisitor.swift new file mode 100644 index 000000000..9560312c9 --- /dev/null +++ b/Sources/TokamakCore/Fiber/App/AppVisitor.swift @@ -0,0 +1,21 @@ +// 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 5/31/22. +// + +/// A type that can visit an `App`. +public protocol AppVisitor: ViewVisitor { + func visit(_ app: A) +} diff --git a/Sources/TokamakCore/Fiber/Fiber+Content.swift b/Sources/TokamakCore/Fiber/Fiber+Content.swift new file mode 100644 index 000000000..68886f164 --- /dev/null +++ b/Sources/TokamakCore/Fiber/Fiber+Content.swift @@ -0,0 +1,65 @@ +// 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 5/31/22. +// + +import Foundation + +public extension FiberReconciler.Fiber { + enum Content { + /// The underlying `App` instance and a function to visit it generically. + case app(Any, visit: (AppVisitor) -> ()) + /// The underlying `Scene` instance and a function to visit it generically. + case scene(Any, visit: (SceneVisitor) -> ()) + /// The underlying `View` instance and a function to visit it generically. + case view(Any, visit: (ViewVisitor) -> ()) + } + + /// Create a `Content` value for a given `App`. + func content(for app: A) -> Content { + .app( + app, + visit: { [weak self] in + guard case let .app(app, _) = self?.content else { return } + // swiftlint:disable:next force_cast + $0.visit(app as! A) + } + ) + } + + /// Create a `Content` value for a given `Scene`. + func content(for scene: S) -> Content { + .scene( + scene, + visit: { [weak self] in + guard case let .scene(scene, _) = self?.content else { return } + // swiftlint:disable:next force_cast + $0.visit(scene as! S) + } + ) + } + + /// Create a `Content` value for a given `View`. + func content(for view: V) -> Content { + .view( + view, + visit: { [weak self] in + guard case let .view(view, _) = self?.content else { return } + // swiftlint:disable:next force_cast + $0.visit(view as! V) + } + ) + } +} diff --git a/Sources/TokamakCore/Fiber/Fiber+CustomDebugStringConvertible.swift b/Sources/TokamakCore/Fiber/Fiber+CustomDebugStringConvertible.swift new file mode 100644 index 000000000..dec9df180 --- /dev/null +++ b/Sources/TokamakCore/Fiber/Fiber+CustomDebugStringConvertible.swift @@ -0,0 +1,43 @@ +// 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 5/30/22. +// + +extension FiberReconciler.Fiber: CustomDebugStringConvertible { + public var debugDescription: String { + if case let .view(view, _) = content, + let text = view as? Text + { + return "Text(\"\(text.storage.rawText)\")" + } + return typeInfo?.name ?? "Unknown" + } + + private func flush(level: Int = 0) -> String { + let spaces = String(repeating: " ", count: level) + let geometry = geometry ?? .init( + origin: .init(origin: .zero), dimensions: .init(size: .zero, alignmentGuides: [:]) + ) + return """ + \(spaces)\(String(describing: typeInfo?.type ?? Any.self) + .split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {\(element != nil ? + "\n\(spaces)geometry: \(geometry)" : + "") + \(child?.flush(level: level + 2) ?? "") + \(spaces)} + \(sibling?.flush(level: level) ?? "") + """ + } +} diff --git a/Sources/TokamakCore/Fiber/Fiber.swift b/Sources/TokamakCore/Fiber/Fiber.swift index 15e49e7a6..4c0c9975b 100644 --- a/Sources/TokamakCore/Fiber/Fiber.swift +++ b/Sources/TokamakCore/Fiber/Fiber.swift @@ -37,16 +37,16 @@ public extension FiberReconciler { /// 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 { + final class Fiber { weak var reconciler: FiberReconciler? - /// The underlying `View` instance. + /// The underlying value behind this `Fiber`. Either a `Scene` or `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. + /// 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 view: Any! + public var content: Content! /// Outputs from evaluating `View._makeView` /// /// Stored as an IUO because creating `ViewOutputs` depends on @@ -54,10 +54,6 @@ public extension FiberReconciler { /// 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. @@ -89,7 +85,7 @@ public extension FiberReconciler { /// The WIP node if this is current, or the current node if this is WIP. weak var alternate: Fiber? - var createAndBindAlternate: (() -> Fiber)? + var createAndBindAlternate: (() -> Fiber?)? /// A box holding a value for an `@State` property wrapper. /// Will call `onSet` (usually a `Reconciler.reconcile` call) when updated. @@ -130,7 +126,6 @@ public extension FiberReconciler { let environment = parent?.outputs.environment ?? .init(.init()) state = bindProperties(to: &view, typeInfo, environment.environment) - self.view = view outputs = V._makeView( .init( content: view, @@ -138,17 +133,13 @@ public extension FiberReconciler { ) ) - visitView = { [weak self] in - guard let self = self else { return } - // swiftlint:disable:next force_cast - $0.visit(self.view as! V) - } + content = content(for: view) if let element = element { self.element = element } else if Renderer.isPrimitive(view) { self.element = .init( - from: .init(from: view, shouldLayout: reconciler?.renderer.shouldLayout ?? false) + from: .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false) ) } @@ -158,7 +149,8 @@ public extension FiberReconciler { } let alternateView = view - createAndBindAlternate = { + createAndBindAlternate = { [weak self] in + guard let self = self else { return nil } // Create the alternate lazily let alternate = Fiber( bound: alternateView, @@ -198,7 +190,6 @@ public extension FiberReconciler { elementParent: Fiber?, reconciler: FiberReconciler? ) { - self.view = view self.alternate = alternate self.reconciler = reconciler self.element = element @@ -208,15 +199,11 @@ public extension FiberReconciler { 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) - } + content = content(for: view) } - private func bindProperties( - to view: inout V, + private func bindProperties( + to content: inout T, _ typeInfo: TypeInfo?, _ environment: EnvironmentValues ) -> [PropertyInfo: MutableStorage] { @@ -224,7 +211,7 @@ public extension FiberReconciler { var state: [PropertyInfo: MutableStorage] = [:] for property in typeInfo.properties where property.type is DynamicProperty.Type { - var value = property.get(from: view) + var value = property.get(from: content) if var storage = value as? WritableValueStorage { let box = MutableStorage(initialValue: storage.anyInitialValue, onSet: { [weak self] in guard let self = self else { return } @@ -238,7 +225,7 @@ public extension FiberReconciler { environmentReader.setContent(from: environment) value = environmentReader } - property.set(value: value, on: &view) + property.set(value: value, on: &content) } return state } @@ -253,46 +240,174 @@ public extension FiberReconciler { let environment = parent?.outputs.environment ?? .init(.init()) state = bindProperties(to: &view, typeInfo, environment.environment) - self.view = view + content = content(for: view) outputs = V._makeView(.init( content: view, environment: environment )) - 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, shouldLayout: reconciler?.renderer.shouldLayout ?? false) + return .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false) } else { return nil } } - public var debugDescription: String { - if let text = view as? Text { - return "Text(\"\(text.storage.rawText)\")" + init( + _ app: inout A, + rootElement: Renderer.ElementType, + rootEnvironment: EnvironmentValues, + reconciler: FiberReconciler + ) { + self.reconciler = reconciler + child = nil + sibling = nil + // `App`s are always the root, so they can have no parent. + parent = nil + elementParent = nil + element = rootElement + typeInfo = TokamakCore.typeInfo(of: A.self) + + state = bindProperties(to: &app, typeInfo, rootEnvironment) + outputs = .init( + inputs: .init(content: app, environment: .init(rootEnvironment)), + layoutComputer: RootLayoutComputer.init + ) + + content = content(for: app) + + let alternateApp = app + createAndBindAlternate = { [weak self] in + guard let self = self else { return nil } + // Create the alternate lazily + let alternate = Fiber( + bound: alternateApp, + alternate: self, + outputs: self.outputs, + typeInfo: self.typeInfo, + element: self.element, + reconciler: reconciler + ) + self.alternate = alternate + return alternate } - return typeInfo?.name ?? "Unknown" } - private func flush(level: Int = 0) -> String { - let spaces = String(repeating: " ", count: level) - let geometry = geometry ?? .init( - origin: .init(origin: .zero), dimensions: .init(size: .zero, alignmentGuides: [:]) + init( + bound app: A, + alternate: Fiber, + outputs: SceneOutputs, + typeInfo: TypeInfo?, + element: Renderer.ElementType?, + reconciler: FiberReconciler? + ) { + self.alternate = alternate + self.reconciler = reconciler + self.element = element + child = nil + sibling = nil + parent = nil + elementParent = nil + self.typeInfo = typeInfo + self.outputs = outputs + content = content(for: app) + } + + init( + _ scene: inout S, + element: Renderer.ElementType?, + parent: Fiber?, + elementParent: Fiber?, + environment: EnvironmentBox?, + reconciler: FiberReconciler? + ) { + self.reconciler = reconciler + child = nil + sibling = nil + self.parent = parent + self.elementParent = elementParent + self.element = element + typeInfo = TokamakCore.typeInfo(of: S.self) + + let environment = environment ?? parent?.outputs.environment ?? .init(.init()) + state = bindProperties(to: &scene, typeInfo, environment.environment) + outputs = S._makeScene( + .init( + content: scene, + environment: environment + ) ) - return """ - \(spaces)\(String(describing: typeInfo?.type ?? Any.self) - .split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {\(element != nil ? - "\n\(spaces)geometry: \(geometry)" : - "") - \(child?.flush(level: level + 2) ?? "") - \(spaces)} - \(sibling?.flush(level: level) ?? "") - """ + + content = content(for: scene) + + let alternateScene = scene + createAndBindAlternate = { [weak self] in + guard let self = self else { return nil } + // Create the alternate lazily + let alternate = Fiber( + bound: alternateScene, + 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( + bound scene: S, + alternate: Fiber, + outputs: SceneOutputs, + typeInfo: TypeInfo?, + element: Renderer.ElementType?, + parent: FiberReconciler.Fiber?, + elementParent: Fiber?, + reconciler: FiberReconciler? + ) { + 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 + content = content(for: scene) + } + + func update( + with scene: inout S + ) -> Renderer.ElementType.Content? { + typeInfo = TokamakCore.typeInfo(of: S.self) + + let environment = parent?.outputs.environment ?? .init(.init()) + state = bindProperties(to: &scene, typeInfo, environment.environment) + content = content(for: scene) + outputs = S._makeScene(.init( + content: scene, + environment: environment + )) + + return nil } } } diff --git a/Sources/TokamakCore/Fiber/FiberElement.swift b/Sources/TokamakCore/Fiber/FiberElement.swift index cefe750ea..06f6feb9c 100644 --- a/Sources/TokamakCore/Fiber/FiberElement.swift +++ b/Sources/TokamakCore/Fiber/FiberElement.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. @@ -29,5 +29,5 @@ public protocol FiberElement: AnyObject { /// 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(from primitiveView: V, shouldLayout: Bool) + init(from primitiveView: V, useDynamicLayout: Bool) } diff --git a/Sources/TokamakCore/Fiber/FiberReconciler+TreeReducer.swift b/Sources/TokamakCore/Fiber/FiberReconciler+TreeReducer.swift index ed3d5da68..6d300d793 100644 --- a/Sources/TokamakCore/Fiber/FiberReconciler+TreeReducer.swift +++ b/Sources/TokamakCore/Fiber/FiberReconciler+TreeReducer.swift @@ -19,11 +19,11 @@ import Foundation extension FiberReconciler { /// Convert the first level of children of a `View` into a linked list of `Fiber`s. - struct TreeReducer: ViewReducer { + struct TreeReducer: SceneReducer { final class Result { // For references let fiber: Fiber? - let visitChildren: (TreeReducer.Visitor) -> () + let visitChildren: (TreeReducer.SceneVisitor) -> () unowned var parent: Result? var child: Result? var sibling: Result? @@ -38,7 +38,7 @@ extension FiberReconciler { init( fiber: Fiber?, - visitChildren: @escaping (TreeReducer.Visitor) -> (), + visitChildren: @escaping (TreeReducer.SceneVisitor) -> (), parent: Result?, child: Fiber?, alternateChild: Fiber?, @@ -57,9 +57,58 @@ extension FiberReconciler { } } + static func reduce(into partialResult: inout Result, nextScene: S) where S: Scene { + Self.reduce( + into: &partialResult, + nextValue: nextScene, + createFiber: { scene, element, parent, elementParent, _, reconciler in + Fiber( + &scene, + element: element, + parent: parent, + elementParent: elementParent, + environment: nil, + reconciler: reconciler + ) + }, + update: { fiber, scene, _ in + fiber.update(with: &scene) + }, + visitChildren: { $0._visitChildren } + ) + } + static func reduce(into partialResult: inout Result, nextView: V) where V: View { + Self.reduce( + into: &partialResult, + nextValue: nextView, + createFiber: { view, element, parent, elementParent, elementIndex, reconciler in + Fiber( + &view, + element: element, + parent: parent, + elementParent: elementParent, + elementIndex: elementIndex, + reconciler: reconciler + ) + }, + update: { fiber, view, elementIndex in + fiber.update(with: &view, elementIndex: elementIndex) + }, + visitChildren: { $0._visitChildren } + ) + } + + static func reduce( + into partialResult: inout Result, + nextValue: T, + createFiber: (inout T, Renderer.ElementType?, Fiber?, Fiber?, Int?, FiberReconciler?) + -> Fiber, + update: (Fiber, inout T, Int?) -> Renderer.ElementType.Content?, + visitChildren: (T) -> (TreeReducer.SceneVisitor) -> () + ) { // Create the node and its element. - var nextView = nextView + var nextValue = nextValue let resultChild: Result if let existing = partialResult.nextExisting { // If a fiber already exists, simply update it with the new view. @@ -69,13 +118,14 @@ extension FiberReconciler { } else { key = nil } - let newContent = existing.update( - with: &nextView, - elementIndex: key.map { partialResult.elementIndices[$0, default: 0] } + let newContent = update( + existing, + &nextValue, + key.map { partialResult.elementIndices[$0, default: 0] } ) resultChild = Result( fiber: existing, - visitChildren: nextView._visitChildren, + visitChildren: visitChildren(nextValue), parent: partialResult, child: existing.child, alternateChild: existing.alternate?.child, @@ -102,13 +152,13 @@ extension FiberReconciler { key = nil } // Otherwise, create a new fiber for this child. - let fiber = Fiber( - &nextView, - element: partialResult.nextExistingAlternate?.element, - parent: partialResult.fiber, - elementParent: elementParent, - elementIndex: key.map { partialResult.elementIndices[$0, default: 0] }, - reconciler: partialResult.fiber?.reconciler + let fiber = createFiber( + &nextValue, + partialResult.nextExistingAlternate?.element, + partialResult.fiber, + elementParent, + key.map { partialResult.elementIndices[$0, default: 0] }, + partialResult.fiber?.reconciler ) // If a fiber already exists for an alternate, link them. if let alternate = partialResult.nextExistingAlternate { @@ -123,7 +173,7 @@ extension FiberReconciler { } resultChild = Result( fiber: fiber, - visitChildren: nextView._visitChildren, + visitChildren: visitChildren(nextValue), parent: partialResult, child: nil, alternateChild: fiber.alternate?.child, diff --git a/Sources/TokamakCore/Fiber/FiberReconciler.swift b/Sources/TokamakCore/Fiber/FiberReconciler.swift index 86c626420..423186ab9 100644 --- a/Sources/TokamakCore/Fiber/FiberReconciler.swift +++ b/Sources/TokamakCore/Fiber/FiberReconciler.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. @@ -70,7 +70,23 @@ public final class FiberReconciler { reconcile(from: current) } - final class ReconcilerVisitor: ViewVisitor { + public init(_ renderer: Renderer, _ app: A) { + self.renderer = renderer + var environment = renderer.defaultEnvironment + environment.measureText = renderer.measureText + var app = app + current = .init( + &app, + rootElement: renderer.rootElement, + rootEnvironment: environment, + reconciler: self + ) + // Start by building the initial tree. + alternate = current.createAndBindAlternate?() + reconcile(from: current) + } + + final class ReconcilerVisitor: AppVisitor, SceneVisitor, ViewVisitor { unowned let reconciler: FiberReconciler /// The current, mounted `Fiber`. var currentRoot: Fiber @@ -82,8 +98,9 @@ public final class FiberReconciler { } /// A `ViewVisitor` that proposes a size for the `View` represented by the fiber `node`. - struct ProposeSizeVisitor: ViewVisitor { + struct ProposeSizeVisitor: AppVisitor, SceneVisitor, ViewVisitor { let node: Fiber + let renderer: Renderer let layoutContexts: [ObjectIdentifier: LayoutContext] func visit(_ view: V) where V: View { @@ -98,6 +115,28 @@ public final class FiberReconciler { node.outputs.layoutComputer = node.outputs.makeLayoutComputer(proposedSize) node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer } + + func visit(_ scene: S) where S: Scene { + node.outputs.layoutComputer = node.outputs.makeLayoutComputer(renderer.sceneSize) + node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer + } + + func visit(_ app: A) where A: App { + node.outputs.layoutComputer = node.outputs.makeLayoutComputer(renderer.sceneSize) + node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer + } + } + + func visit(_ view: V) where V: View { + visitAny(view, visitChildren: view._visitChildren) + } + + func visit(_ scene: S) where S: Scene { + visitAny(scene, visitChildren: scene._visitChildren) + } + + func visit(_ app: A) where A: App { + visitAny(app, visitChildren: { $0.visit(app.body) }) } /// Walk the current tree, recomputing at each step to check for discrepancies. @@ -142,7 +181,10 @@ public final class FiberReconciler { /// │ │Text│ /// └───────┴────┘ /// ``` - func visit(_ view: V) where V: View { + private func visitAny( + _ value: Any, + visitChildren: @escaping (TreeReducer.SceneVisitor) -> () + ) { let alternateRoot: Fiber? if let alternate = currentRoot.alternate { alternateRoot = alternate @@ -151,7 +193,7 @@ public final class FiberReconciler { } let rootResult = TreeReducer.Result( fiber: alternateRoot, // The alternate is the WIP node. - visitChildren: view._visitChildren, + visitChildren: visitChildren, parent: nil, child: alternateRoot?.child, alternateChild: currentRoot.child, @@ -182,7 +224,8 @@ public final class FiberReconciler { let previous = node.fiber?.alternate?.element { // This is a completely different type of view. - mutations.append(.replace(parent: parent, previous: previous, replacement: element)) + mutations + .append(.replace(parent: parent, previous: previous, replacement: element)) } else if let newContent = node.newContent, newContent != element.content { @@ -203,8 +246,22 @@ public final class FiberReconciler { func proposeSize(for node: Fiber) { guard node.element != nil else { return } - // Use the visitor so we can pass the correct View type to the function. - node.visitView(ProposeSizeVisitor(node: node, layoutContexts: layoutContexts)) + // Use a visitor so we can pass the correct `View`/`Scene` type to the function. + let visitor = ProposeSizeVisitor( + node: node, + renderer: reconciler.renderer, + layoutContexts: layoutContexts + ) + switch node.content { + case let .view(_, visit): + visit(visitor) + case let .scene(_, visit): + visit(visitor) + case let .app(_, visit): + visit(visitor) + case .none: + break + } } /// Request a size from the fiber's `elementParent`. @@ -276,12 +333,12 @@ public final class FiberReconciler { node.elementIndices = elementIndices // Compute the children of the node. - let reducer = TreeReducer.Visitor(initialResult: node) + let reducer = TreeReducer.SceneVisitor(initialResult: node) node.visitChildren(reducer) elementIndices = node.elementIndices // As we walk down the tree, propose a size for each View. - if reconciler.renderer.shouldLayout, + if reconciler.renderer.useDynamicLayout, let fiber = node.fiber { proposeSize(for: fiber) @@ -328,7 +385,9 @@ public final class FiberReconciler { // 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. + while alternateSibling != + nil + { // The alternate had siblings that no longer exist. if let element = alternateSibling?.element, let parent = alternateSibling?.elementParent?.element { @@ -339,7 +398,7 @@ public final class FiberReconciler { alternateSibling = alternateSibling?.sibling } // We `size` and `position` when we are walking back up the tree. - if reconciler.renderer.shouldLayout, + if reconciler.renderer.useDynamicLayout, let fiber = node.fiber { // The `elementParent` proposed a size for this fiber on the way down. @@ -362,10 +421,11 @@ public final class FiberReconciler { node = parent } - // We also request `size` and `position` when we reach the bottom-most view that has a sibling. + // We also request `size` and `position` when we reach the bottom-most view + // that has a sibling. // Sizing and positioning also happen when we have no sibling, // as seen in the above loop. - if reconciler.renderer.shouldLayout, + if reconciler.renderer.useDynamicLayout, let fiber = node.fiber { // Request a size from our `elementParent`. @@ -384,7 +444,7 @@ public final class FiberReconciler { } mainLoop() - if reconciler.renderer.shouldLayout { + if reconciler.renderer.useDynamicLayout { // We continue to the very top to update all necessary positions. var layoutNode = node.fiber?.child while let current = layoutNode { @@ -409,7 +469,16 @@ public final class FiberReconciler { func reconcile(from root: Fiber) { // Create a list of mutations. let visitor = ReconcilerVisitor(root: root, reconciler: self) - root.visitView(visitor) + switch root.content { + case let .view(_, visit): + visit(visitor) + case let .scene(_, visit): + visit(visitor) + case let .app(_, visit): + visit(visitor) + case .none: + break + } // Apply mutations to the rendered output. renderer.commit(visitor.mutations) diff --git a/Sources/TokamakCore/Fiber/FiberRenderer.swift b/Sources/TokamakCore/Fiber/FiberRenderer.swift index 7f05564d5..9b99394bf 100644 --- a/Sources/TokamakCore/Fiber/FiberRenderer.swift +++ b/Sources/TokamakCore/Fiber/FiberRenderer.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. @@ -32,7 +32,7 @@ public protocol FiberRenderer { /// The size of the window we are rendering in. var sceneSize: CGSize { get } /// Whether layout is enabled for this renderer. - var shouldLayout: Bool { get } + var useDynamicLayout: Bool { get } /// Calculate the size of `Text` in `environment` for layout. func measureText(_ text: Text, proposedSize: CGSize, in environment: EnvironmentValues) -> CGSize } @@ -44,6 +44,11 @@ public extension FiberRenderer { func render(_ view: V) -> FiberReconciler { .init(self, view) } + + @discardableResult + func render(_ app: A) -> FiberReconciler { + .init(self, app) + } } extension EnvironmentValues { diff --git a/Sources/TokamakCore/Fiber/LayoutComputer.swift b/Sources/TokamakCore/Fiber/LayoutComputer.swift index d449d6a5a..da257ac60 100644 --- a/Sources/TokamakCore/Fiber/LayoutComputer.swift +++ b/Sources/TokamakCore/Fiber/LayoutComputer.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. diff --git a/Sources/TokamakCore/Fiber/Mutation.swift b/Sources/TokamakCore/Fiber/Mutation.swift index 5c2e28945..e7206cf3b 100644 --- a/Sources/TokamakCore/Fiber/Mutation.swift +++ b/Sources/TokamakCore/Fiber/Mutation.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. diff --git a/Sources/TokamakCore/Fiber/Scene/SceneArguments.swift b/Sources/TokamakCore/Fiber/Scene/SceneArguments.swift new file mode 100644 index 000000000..fdb943c7e --- /dev/null +++ b/Sources/TokamakCore/Fiber/Scene/SceneArguments.swift @@ -0,0 +1,28 @@ +// 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 5/30/22. +// + +import Foundation + +public extension Scene { + // By default, we simply pass the inputs through without modifications. + static func _makeScene(_ inputs: SceneInputs) -> SceneOutputs { + .init( + inputs: inputs, + layoutComputer: RootLayoutComputer.init + ) + } +} diff --git a/Sources/TokamakCore/Fiber/Scene/SceneVisitor.swift b/Sources/TokamakCore/Fiber/Scene/SceneVisitor.swift new file mode 100644 index 000000000..51b2e78ec --- /dev/null +++ b/Sources/TokamakCore/Fiber/Scene/SceneVisitor.swift @@ -0,0 +1,68 @@ +// 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 5/30/22. +// + +/// A type that can visit a `Scene`. +public protocol SceneVisitor: ViewVisitor { + func visit(_ scene: S) +} + +public extension Scene { + func _visitChildren(_ visitor: V) { + visitor.visit(body) + } +} + +/// A type that creates a `Result` by visiting multiple `Scene`s. +protocol SceneReducer: ViewReducer { + associatedtype Result + static func reduce(into partialResult: inout Result, nextScene: S) + static func reduce(partialResult: Result, nextScene: S) -> Result +} + +extension SceneReducer { + static func reduce(into partialResult: inout Result, nextScene: S) { + partialResult = Self.reduce(partialResult: partialResult, nextScene: nextScene) + } + + static func reduce(partialResult: Result, nextScene: S) -> Result { + var result = partialResult + Self.reduce(into: &result, nextScene: nextScene) + return result + } +} + +/// A `SceneVisitor` that uses a `SceneReducer` +/// to collapse the `Scene` values into a single `Result`. +final class SceneReducerVisitor: SceneVisitor { + var result: R.Result + + init(initialResult: R.Result) { + result = initialResult + } + + func visit(_ scene: S) where S: Scene { + R.reduce(into: &result, nextScene: scene) + } + + func visit(_ view: V) where V: View { + R.reduce(into: &result, nextView: view) + } +} + +extension SceneReducer { + typealias SceneVisitor = SceneReducerVisitor +} diff --git a/Sources/TokamakCore/Fiber/ViewArguments.swift b/Sources/TokamakCore/Fiber/ViewArguments.swift index 82a696b68..e592a10a8 100644 --- a/Sources/TokamakCore/Fiber/ViewArguments.swift +++ b/Sources/TokamakCore/Fiber/ViewArguments.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. diff --git a/Sources/TokamakCore/Fiber/ViewVisitor.swift b/Sources/TokamakCore/Fiber/ViewVisitor.swift index 5f972238f..c4fe58f37 100644 --- a/Sources/TokamakCore/Fiber/ViewVisitor.swift +++ b/Sources/TokamakCore/Fiber/ViewVisitor.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. @@ -15,6 +15,7 @@ // Created by Carson Katri on 2/3/22. // +/// A type that can visit a `View`. public protocol ViewVisitor { func visit(_ view: V) } @@ -27,6 +28,7 @@ public extension View { typealias ViewVisitorF = (V) -> () +/// A type that creates a `Result` by visiting multiple `View`s. protocol ViewReducer { associatedtype Result static func reduce(into partialResult: inout Result, nextView: V) @@ -45,6 +47,8 @@ extension ViewReducer { } } +/// A `ViewVisitor` that uses a `ViewReducer` +/// to collapse the `View` values into a single `Result`. final class ReducerVisitor: ViewVisitor { var result: R.Result diff --git a/Sources/TokamakCore/Fiber/walk.swift b/Sources/TokamakCore/Fiber/walk.swift index 0c7b49628..91ec887f0 100644 --- a/Sources/TokamakCore/Fiber/walk.swift +++ b/Sources/TokamakCore/Fiber/walk.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Tokamak contributors +// 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. diff --git a/Sources/TokamakCore/Views/View.swift b/Sources/TokamakCore/Views/View.swift index 0bb5528f9..08b0a222f 100644 --- a/Sources/TokamakCore/Views/View.swift +++ b/Sources/TokamakCore/Views/View.swift @@ -21,7 +21,12 @@ public protocol View { @ViewBuilder var body: Self.Body { get } + /// Override the default implementation for `View`s with body types of `Never` + /// or in cases where the body would normally need to be type erased. func _visitChildren(_ visitor: V) + + /// Create `ViewOutputs`, including any modifications to the environment, preferences, or a custom + /// `LayoutComputer` from the `ViewInputs`. static func _makeView(_ inputs: ViewInputs) -> ViewOutputs } diff --git a/Sources/TokamakCoreBenchmark/main.swift b/Sources/TokamakCoreBenchmark/main.swift index 66d0dae8c..351a476bc 100644 --- a/Sources/TokamakCoreBenchmark/main.swift +++ b/Sources/TokamakCoreBenchmark/main.swift @@ -66,9 +66,9 @@ benchmark("update wide (FiberReconciler)") { state in let reconciler = TestFiberRenderer( .root, size: .init(width: 500, height: 500), - shouldLayout: false + useDynamicLayout: false ).render(view) - let button = reconciler.current // RootView + guard case let .view(view, _) = reconciler.current // RootView .child? // ModifiedContent .child? // _ViewModifier_Content .child? // UpdateLast @@ -78,9 +78,12 @@ benchmark("update wide (FiberReconciler)") { state in .child? // ConditionalContent .child? // AnyView .child? // _PrimitiveButtonStyleBody - .view + .content, + let button = view as? _PrimitiveButtonStyleBody + else { return } + try state.measure { - (button as? _PrimitiveButtonStyleBody)?.action() + button.action() } } @@ -124,9 +127,9 @@ benchmark("update narrow (FiberReconciler)") { state in let reconciler = TestFiberRenderer( .root, size: .init(width: 500, height: 500), - shouldLayout: false + useDynamicLayout: false ).render(view) - let button = reconciler.current // RootView + guard case let .view(view, _) = reconciler.current // RootView .child? // ModifiedContent .child? // _ViewModifier_Content .child? // UpdateLast @@ -136,9 +139,11 @@ benchmark("update narrow (FiberReconciler)") { state in .child? // ConditionalContent .child? // AnyView .child? // _PrimitiveButtonStyleBody - .view + .content, + let button = view as? _PrimitiveButtonStyleBody + else { return } try state.measure { - (button as? _PrimitiveButtonStyleBody)?.action() + button.action() } } @@ -194,9 +199,9 @@ benchmark("update deep (FiberReconciler)") { state in let reconciler = TestFiberRenderer( .root, size: .init(width: 500, height: 500), - shouldLayout: false + useDynamicLayout: false ).render(view) - let button = reconciler.current // RootView + guard case let .view(view, _) = reconciler.current // RootView .child? // ModifiedContent .child? // _ViewModifier_Content .child? // UpdateLast @@ -206,9 +211,11 @@ benchmark("update deep (FiberReconciler)") { state in .child? // ConditionalContent .child? // AnyView .child? // _PrimitiveButtonStyleBody - .view + .content, + let button = view as? _PrimitiveButtonStyleBody + else { return } try state.measure { - (button as? _PrimitiveButtonStyleBody)?.action() + button.action() } } @@ -262,9 +269,9 @@ benchmark("update shallow (FiberReconciler)") { _ in let reconciler = TestFiberRenderer( .root, size: .init(width: 500, height: 500), - shouldLayout: false + useDynamicLayout: false ).render(view) - let button = reconciler.current // RootView + guard case let .view(view, _) = reconciler.current // RootView .child? // ModifiedContent .child? // _ViewModifier_Content .child? // UpdateLast @@ -274,9 +281,11 @@ benchmark("update shallow (FiberReconciler)") { _ in .child? // ConditionalContent .child? // AnyView .child? // _PrimitiveButtonStyleBody - .view + .content, + let button = view as? _PrimitiveButtonStyleBody + else { return } // Using state.measure here hangs the benchmark app?g - (button as? _PrimitiveButtonStyleBody)?.action() + button.action() } Benchmark.main() diff --git a/Sources/TokamakDOM/App/App.swift b/Sources/TokamakDOM/App/App.swift index d679d6fba..da786ee19 100644 --- a/Sources/TokamakDOM/App/App.swift +++ b/Sources/TokamakDOM/App/App.swift @@ -21,8 +21,13 @@ import TokamakCore import TokamakStaticHTML public extension App { - static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { - _launch(app, rootEnvironment, TokamakDOM.body) + static func _launch(_ app: Self, with configuration: _AppConfiguration) { + switch configuration.reconciler { + case .stack: + _launch(app, configuration.rootEnvironment, TokamakDOM.body) + case let .fiber(useDynamicLayout): + DOMFiberRenderer("body", useDynamicLayout: useDynamicLayout).render(app) + } } /// The default implementation of `launch` for a `TokamakDOM` app. diff --git a/Sources/TokamakDOM/Core.swift b/Sources/TokamakDOM/Core.swift index a0e8d5eda..b2d981899 100644 --- a/Sources/TokamakDOM/Core.swift +++ b/Sources/TokamakDOM/Core.swift @@ -193,6 +193,7 @@ public typealias TextAlignment = TokamakCore.TextAlignment // MARK: App & Scene public typealias App = TokamakCore.App +public typealias _AppConfiguration = TokamakCore._AppConfiguration public typealias Scene = TokamakCore.Scene public typealias WindowGroup = TokamakCore.WindowGroup public typealias ScenePhase = TokamakCore.ScenePhase diff --git a/Sources/TokamakDOM/DOMFiberRenderer.swift b/Sources/TokamakDOM/DOMFiberRenderer.swift index e74e7e514..afd4e1276 100644 --- a/Sources/TokamakDOM/DOMFiberRenderer.swift +++ b/Sources/TokamakDOM/DOMFiberRenderer.swift @@ -49,10 +49,10 @@ public final class DOMElement: FiberElement { } public extension DOMElement.Content { - init(from primitiveView: V, shouldLayout: Bool) where V: View { + init(from primitiveView: V, useDynamicLayout: Bool) where V: View { guard let primitiveView = primitiveView as? HTMLConvertible else { fatalError() } tag = primitiveView.tag - attributes = primitiveView.attributes(shouldLayout: shouldLayout) + attributes = primitiveView.attributes(useDynamicLayout: useDynamicLayout) innerHTML = primitiveView.innerHTML if let primitiveView = primitiveView as? DOMNodeConvertible { @@ -78,7 +78,7 @@ public struct DOMFiberRenderer: FiberRenderer { .init(width: body.clientWidth.number!, height: body.clientHeight.number!) } - public let shouldLayout: Bool + public let useDynamicLayout: Bool public var defaultEnvironment: EnvironmentValues { var environment = EnvironmentValues() @@ -86,7 +86,7 @@ public struct DOMFiberRenderer: FiberRenderer { return environment } - public init(_ rootSelector: String, shouldLayout: Bool = true) { + public init(_ rootSelector: String, useDynamicLayout: Bool = true) { guard let reference = document.querySelector!(rootSelector).object else { fatalError(""" The root element with selector '\(rootSelector)' could not be found. \ @@ -103,9 +103,9 @@ public struct DOMFiberRenderer: FiberRenderer { ) ) rootElement.reference = reference - self.shouldLayout = shouldLayout + self.useDynamicLayout = useDynamicLayout - if shouldLayout { + if useDynamicLayout { // Setup the root styles _ = reference.style.setProperty("margin", "0") _ = reference.style.setProperty("width", "100vw") @@ -130,7 +130,7 @@ public struct DOMFiberRenderer: FiberRenderer { proposedSize: CGSize, in environment: EnvironmentValues ) -> CGSize { - let element = createElement(.init(from: .init(from: text, shouldLayout: true))) + let element = createElement(.init(from: .init(from: text, useDynamicLayout: true))) _ = element.style.setProperty("maxWidth", "\(proposedSize.width)px") _ = element.style.setProperty("maxHeight", "\(proposedSize.height)px") _ = document.body.appendChild(element) @@ -166,7 +166,7 @@ public struct DOMFiberRenderer: FiberRenderer { } private func apply(_ geometry: ViewGeometry, to element: JSObject) { - guard shouldLayout else { return } + guard useDynamicLayout else { return } _ = element.style.setProperty("position", "absolute") _ = element.style.setProperty("width", "\(geometry.dimensions.width)px") _ = element.style.setProperty("height", "\(geometry.dimensions.height)px") @@ -216,7 +216,7 @@ public struct DOMFiberRenderer: FiberRenderer { extension _PrimitiveButtonStyleBody: DOMNodeConvertible { public var tag: String { "button" } - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { [:] } diff --git a/Sources/TokamakGTK/App/App.swift b/Sources/TokamakGTK/App/App.swift index 1dc13e6de..891477bfe 100644 --- a/Sources/TokamakGTK/App/App.swift +++ b/Sources/TokamakGTK/App/App.swift @@ -21,8 +21,8 @@ import OpenCombineShim import TokamakCore public extension App { - static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { - _ = Unmanaged.passRetained(GTKRenderer(app, rootEnvironment)) + static func _launch(_ app: Self, with configuration: _AppConfiguration) { + _ = Unmanaged.passRetained(GTKRenderer(app, configuration.rootEnvironment)) } static func _setTitle(_ title: String) { diff --git a/Sources/TokamakStaticHTML/App.swift b/Sources/TokamakStaticHTML/App.swift index c99e667bc..e027b4cd1 100644 --- a/Sources/TokamakStaticHTML/App.swift +++ b/Sources/TokamakStaticHTML/App.swift @@ -19,7 +19,7 @@ import OpenCombineShim import TokamakCore public extension App { - static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { + static func _launch(_ app: Self, with configuration: _AppConfiguration) { fatalError("TokamakStaticHTML does not support default `App._launch`") } diff --git a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift index f472f4235..7f5a4e6fe 100644 --- a/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift +++ b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift @@ -76,8 +76,8 @@ extension _FrameLayout: DOMViewModifier { @_spi(TokamakStaticHTML) extension _FrameLayout: HTMLConvertible { public var tag: String { "div" } - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { - guard !shouldLayout else { return [ + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { + guard !useDynamicLayout else { return [ "style": "overflow: hidden;", ] } return attributes diff --git a/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift index 6a7c135a5..4972143d8 100644 --- a/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift +++ b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift @@ -50,8 +50,8 @@ extension ModifiedContent: HTMLConvertible where Content: View, Modifier: HTMLConvertible { public var tag: String { modifier.tag } - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { - modifier.attributes(shouldLayout: shouldLayout) + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { + modifier.attributes(useDynamicLayout: useDynamicLayout) } public var innerHTML: String? { modifier.innerHTML } diff --git a/Sources/TokamakStaticHTML/StaticHTMLFiberRenderer.swift b/Sources/TokamakStaticHTML/StaticHTMLFiberRenderer.swift index aff8438d2..a65b73f70 100644 --- a/Sources/TokamakStaticHTML/StaticHTMLFiberRenderer.swift +++ b/Sources/TokamakStaticHTML/StaticHTMLFiberRenderer.swift @@ -33,10 +33,10 @@ public final class HTMLElement: FiberElement, CustomStringConvertible { var innerHTML: String? var children: [HTMLElement] = [] - public init(from primitiveView: V, shouldLayout: Bool) where V: View { + public init(from primitiveView: V, useDynamicLayout: Bool) where V: View { guard let primitiveView = primitiveView as? HTMLConvertible else { fatalError() } tag = primitiveView.tag - attributes = primitiveView.attributes(shouldLayout: shouldLayout) + attributes = primitiveView.attributes(useDynamicLayout: useDynamicLayout) innerHTML = primitiveView.innerHTML } @@ -92,7 +92,7 @@ public final class HTMLElement: FiberElement, CustomStringConvertible { @_spi(TokamakStaticHTML) public protocol HTMLConvertible { var tag: String { get } - func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] + func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] var innerHTML: String? { get } } @@ -107,7 +107,7 @@ extension VStack: HTMLConvertible { public var tag: String { "div" } @_spi(TokamakStaticHTML) - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { let spacing = _VStackProxy(self).spacing return [ "style": """ @@ -127,7 +127,7 @@ extension HStack: HTMLConvertible { public var tag: String { "div" } @_spi(TokamakStaticHTML) - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { let spacing = _HStackProxy(self).spacing return [ "style": """ @@ -145,7 +145,7 @@ public struct StaticHTMLFiberRenderer: FiberRenderer { public let rootElement: HTMLElement public let defaultEnvironment: EnvironmentValues public let sceneSize: CGSize = .zero - public let shouldLayout: Bool = false + public let useDynamicLayout: Bool = false public init() { rootElement = .init(tag: "body", attributes: [:], innerHTML: nil, children: []) diff --git a/Sources/TokamakStaticHTML/Views/Text/Text.swift b/Sources/TokamakStaticHTML/Views/Text/Text.swift index 11a4ab53b..cfb9b86ba 100644 --- a/Sources/TokamakStaticHTML/Views/Text/Text.swift +++ b/Sources/TokamakStaticHTML/Views/Text/Text.swift @@ -166,7 +166,7 @@ extension Text: HTMLConvertible { innerHTML(shouldSortAttributes: false) } - public func attributes(shouldLayout: Bool) -> [HTMLAttribute: String] { + public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] { attributes } } diff --git a/Sources/TokamakTestRenderer/App.swift b/Sources/TokamakTestRenderer/App.swift index 42738bef4..79a82bbde 100644 --- a/Sources/TokamakTestRenderer/App.swift +++ b/Sources/TokamakTestRenderer/App.swift @@ -18,7 +18,7 @@ import TokamakCore public extension App { static func _setTitle(_ title: String) {} - static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {} + static func _launch(_ app: Self, with configuration: _AppConfiguration) {} var _phasePublisher: AnyPublisher { Empty().eraseToAnyPublisher() } diff --git a/Sources/TokamakTestRenderer/TestFiberRenderer.swift b/Sources/TokamakTestRenderer/TestFiberRenderer.swift index 017425ef8..8e201cba2 100644 --- a/Sources/TokamakTestRenderer/TestFiberRenderer.swift +++ b/Sources/TokamakTestRenderer/TestFiberRenderer.swift @@ -83,7 +83,7 @@ public final class TestFiberElement: FiberElement, CustomStringConvertible { self.closingTag = closingTag } - public init(from primitiveView: V, shouldLayout: Bool) where V: View { + public init(from primitiveView: V, useDynamicLayout: Bool) where V: View { guard let primitiveView = primitiveView as? TestFiberPrimitive else { fatalError() } let attributes = primitiveView.attributes .sorted(by: { $0.key < $1.key }) @@ -118,7 +118,7 @@ public final class TestFiberElement: FiberElement, CustomStringConvertible { public struct TestFiberRenderer: FiberRenderer { public let sceneSize: CGSize - public let shouldLayout: Bool + public let useDynamicLayout: Bool public func measureText( _ text: Text, @@ -132,10 +132,10 @@ public struct TestFiberRenderer: FiberRenderer { public let rootElement: ElementType - public init(_ rootElement: ElementType, size: CGSize, shouldLayout: Bool = true) { + public init(_ rootElement: ElementType, size: CGSize, useDynamicLayout: Bool = true) { self.rootElement = rootElement sceneSize = size - self.shouldLayout = shouldLayout + self.useDynamicLayout = useDynamicLayout } public static func isPrimitive(_ view: V) -> Bool where V: View { diff --git a/Tests/TokamakReconcilerTests/VisitorTests.swift b/Tests/TokamakReconcilerTests/VisitorTests.swift index 0d600a368..4d7a763af 100644 --- a/Tests/TokamakReconcilerTests/VisitorTests.swift +++ b/Tests/TokamakReconcilerTests/VisitorTests.swift @@ -59,40 +59,37 @@ final class VisitorTests: XCTestCase { let reconciler = TestFiberRenderer(.root, size: .init(width: 500, height: 500)) .render(TestView()) func decrement() { - ( - reconciler.current // RootView - .child? // ModifiedContent - .child? // _ViewModifier_Content - .child? // TestView - .child? // Counter - .child? // VStack - .child? // TupleView - .child?.sibling? // HStack - .child? // TupleView - .child? // Optional - .child? // Button - .view as? Button - )? - .action() + guard case let .view(view, _) = reconciler.current // RootView + .child? // ModifiedContent + .child? // _ViewModifier_Content + .child? // TestView + .child? // Counter + .child? // VStack + .child? // TupleView + .child?.sibling? // HStack + .child? // TupleView + .child? // Optional + .child? // Button + .content + else { return } + (view as? Button)?.action() } func increment() { - ( - reconciler.current // RootView - .child? // 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 - )? - .action() + guard case let .view(view, _) = reconciler.current // RootView + .child? // ModifiedContent + .child? // _ViewModifier_Content + .child? // TestView + .child? // Counter + .child? // VStack + .child? // TupleView + .child?.sibling? // HStack + .child? // TupleView + .child? // Optional + .sibling? // Optional + .child? // Button + .content + else { return } + (view as? Button)?.action() } for _ in 0..<5 { increment()