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:
parent
9db23c9e3f
commit
6e2ccf71ea
25
README.md
25
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) ?? "")
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] {
|
||||
[:]
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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`")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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: [])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue