Add configuration options to `App` to choose reconciler (#495)

* Add support for App/Scene

* Refine reconciler configuration options

* Fix tests

* Fix benchmark

* Address review comments

* Rename shouldLayout to useDynamicLayout in remaining file

* Add note to README about switching reconcilers

* Fix typo

* Add App as a type of Fiber content

* Refactor to avoid temporary variable

* Address review comments

* Add [weak self] to createAndBindAlternate closures

* Remove commented lines
This commit is contained in:
Carson Katri 2022-06-05 19:24:05 -04:00 committed by GitHub
parent 9db23c9e3f
commit 6e2ccf71ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 834 additions and 190 deletions

View File

@ -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

View File

@ -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<ScenePhase, Never> { get }
@ -36,14 +39,38 @@ public protocol App: _TitledApp {
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
static var _configuration: _AppConfiguration { get }
static func main()
init()
}
public extension App {
static func main() {
let app = Self()
_launch(app, EnvironmentValues())
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, with: Self._configuration)
}
}

View File

@ -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<V: SceneVisitor>(_ 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<Self>) -> SceneOutputs
}
public typealias SceneInputs<S: Scene> = ViewInputs<S>
public typealias SceneOutputs = ViewOutputs
protocol TitledScene {
var title: Text? { get }
}

View File

@ -29,7 +29,14 @@ public extension SceneBuilder {
static func buildBlock<C0, C1>(_ 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, C1, C2>(_ 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)
}
)
}
}

View File

@ -75,4 +75,8 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
// public init(_ titleKey: LocalizedStringKey,
// @ViewBuilder content: () -> Content) {
// }
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
visitor.visit(content)
}
}

View File

@ -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<ScenePhase, Never> {
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")

View File

@ -17,11 +17,17 @@
struct _TupleScene<T>: 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 {

View File

@ -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<A: App>(_ app: A)
}

View File

@ -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<A: App>(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<S: Scene>(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<V: View>(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)
}
)
}
}

View File

@ -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) ?? "")
"""
}
}

View File

@ -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<Renderer>?
/// 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<Renderer>?
) {
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<V: View>(
to view: inout V,
private func bindProperties<T>(
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<A: App>(
_ app: inout A,
rootElement: Renderer.ElementType,
rootEnvironment: EnvironmentValues,
reconciler: FiberReconciler<Renderer>
) {
self.reconciler = reconciler
child = nil
sibling = nil
// `App`s are always the root, so they can have no parent.
parent = nil
elementParent = nil
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<A: App>(
bound app: A,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
parent = nil
elementParent = nil
self.typeInfo = typeInfo
self.outputs = outputs
content = content(for: app)
}
init<S: Scene>(
_ scene: inout S,
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
environment: EnvironmentBox?,
reconciler: FiberReconciler<Renderer>?
) {
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<S: Scene>(
bound scene: S,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
parent: FiberReconciler<Renderer>.Fiber?,
elementParent: Fiber?,
reconciler: FiberReconciler<Renderer>?
) {
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<S: Scene>(
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
}
}
}

View File

@ -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<V: View>(from primitiveView: V, shouldLayout: Bool)
init<V: View>(from primitiveView: V, useDynamicLayout: Bool)
}

View File

@ -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<S>(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<V>(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<T>(
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,

View File

@ -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<Renderer: FiberRenderer> {
reconcile(from: current)
}
final class ReconcilerVisitor: ViewVisitor {
public init<A: App>(_ 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<Renderer>
/// The current, mounted `Fiber`.
var currentRoot: Fiber
@ -82,8 +98,9 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
}
/// 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<V>(_ view: V) where V: View {
@ -98,6 +115,28 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
node.outputs.layoutComputer = node.outputs.makeLayoutComputer(proposedSize)
node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer
}
func visit<S>(_ scene: S) where S: Scene {
node.outputs.layoutComputer = node.outputs.makeLayoutComputer(renderer.sceneSize)
node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer
}
func visit<A>(_ app: A) where A: App {
node.outputs.layoutComputer = node.outputs.makeLayoutComputer(renderer.sceneSize)
node.alternate?.outputs.layoutComputer = node.outputs.layoutComputer
}
}
func visit<V>(_ view: V) where V: View {
visitAny(view, visitChildren: view._visitChildren)
}
func visit<S>(_ scene: S) where S: Scene {
visitAny(scene, visitChildren: scene._visitChildren)
}
func visit<A>(_ 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<Renderer: FiberRenderer> {
/// Text
///
/// ```
func visit<V>(_ 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<Renderer: FiberRenderer> {
}
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<Renderer: FiberRenderer> {
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<Renderer: FiberRenderer> {
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<Renderer: FiberRenderer> {
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<Renderer: FiberRenderer> {
// 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<Renderer: FiberRenderer> {
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<Renderer: FiberRenderer> {
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<Renderer: FiberRenderer> {
}
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<Renderer: FiberRenderer> {
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)

View File

@ -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<V: View>(_ view: V) -> FiberReconciler<Self> {
.init(self, view)
}
@discardableResult
func render<A: App>(_ app: A) -> FiberReconciler<Self> {
.init(self, app)
}
}
extension EnvironmentValues {

View File

@ -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.

View File

@ -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.

View File

@ -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<Self>) -> SceneOutputs {
.init(
inputs: inputs,
layoutComputer: RootLayoutComputer.init
)
}
}

View File

@ -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<S: Scene>(_ scene: S)
}
public extension Scene {
func _visitChildren<V: SceneVisitor>(_ visitor: V) {
visitor.visit(body)
}
}
/// A type that creates a `Result` by visiting multiple `Scene`s.
protocol SceneReducer: ViewReducer {
associatedtype Result
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S)
static func reduce<S: Scene>(partialResult: Result, nextScene: S) -> Result
}
extension SceneReducer {
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S) {
partialResult = Self.reduce(partialResult: partialResult, nextScene: nextScene)
}
static func reduce<S: Scene>(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<R: SceneReducer>: SceneVisitor {
var result: R.Result
init(initialResult: R.Result) {
result = initialResult
}
func visit<S>(_ scene: S) where S: Scene {
R.reduce(into: &result, nextScene: scene)
}
func visit<V>(_ view: V) where V: View {
R.reduce(into: &result, nextView: view)
}
}
extension SceneReducer {
typealias SceneVisitor = SceneReducerVisitor<Self>
}

View File

@ -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.

View File

@ -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<V: View>(_ view: V)
}
@ -27,6 +28,7 @@ public extension View {
typealias ViewVisitorF<V: ViewVisitor> = (V) -> ()
/// A type that creates a `Result` by visiting multiple `View`s.
protocol ViewReducer {
associatedtype Result
static func reduce<V: View>(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<R: ViewReducer>: ViewVisitor {
var result: R.Result

View File

@ -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.

View File

@ -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<V: ViewVisitor>(_ visitor: V)
/// Create `ViewOutputs`, including any modifications to the environment, preferences, or a custom
/// `LayoutComputer` from the `ViewInputs`.
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
}

View File

@ -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<PrimitiveButtonStyleConfiguration.Label>
.view
.content,
let button = view as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
else { return }
try state.measure {
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.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<PrimitiveButtonStyleConfiguration.Label>
.view
.content,
let button = view as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
else { return }
try state.measure {
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.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<PrimitiveButtonStyleConfiguration.Label>
.view
.content,
let button = view as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
else { return }
try state.measure {
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.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<PrimitiveButtonStyleConfiguration.Label>
.view
.content,
let button = view as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>
else { return }
// Using state.measure here hangs the benchmark app?g
(button as? _PrimitiveButtonStyleBody<PrimitiveButtonStyleConfiguration.Label>)?.action()
button.action()
}
Benchmark.main()

View File

@ -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.

View File

@ -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

View File

@ -49,10 +49,10 @@ public final class DOMElement: FiberElement {
}
public extension DOMElement.Content {
init<V>(from primitiveView: V, shouldLayout: Bool) where V: View {
init<V>(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] {
[:]
}

View File

@ -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) {

View File

@ -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`")
}

View File

@ -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

View File

@ -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 }

View File

@ -33,10 +33,10 @@ public final class HTMLElement: FiberElement, CustomStringConvertible {
var innerHTML: String?
var children: [HTMLElement] = []
public init<V>(from primitiveView: V, shouldLayout: Bool) where V: View {
public init<V>(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: [])

View File

@ -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
}
}

View File

@ -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<ScenePhase, Never> { Empty().eraseToAnyPublisher() }

View File

@ -83,7 +83,7 @@ public final class TestFiberElement: FiberElement, CustomStringConvertible {
self.closingTag = closingTag
}
public init<V>(from primitiveView: V, shouldLayout: Bool) where V: View {
public init<V>(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<V>(_ view: V) -> Bool where V: View {

View File

@ -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<Text>
)?
.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<Text>)?.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<Text>
)?
.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<Text>)?.action()
}
for _ in 0..<5 {
increment()