Skip to content

Commit

Permalink
Custom Layout Engine for Fiber Reconciler (#472)
Browse files Browse the repository at this point in the history
* Initial Reconciler using visitor pattern

* Preliminary static HTML renderer using the new reconciler

* Add environment

* Initial DOM renderer

* Nearly-working and simplified reconciler

* Working reconciler for HTML/DOM renderers

* Rename files, and split code across files

* Add some documentation and refinements

* Remove GraphRendererTests

* Initial layout engine (only implemented for the TestRenderer)

* Layout engine for the DOM renderer

* Refined layout pass

* Revise positioning and restoration of position styles on .update

* Re-add Optional.body for StackReconciler-based renderers

* Add text measurement

* Add spacing to StackLayout

* Add benchmarks to compare the stack/fiber reconcilers

* Fix some issues created for the StackReconciler, and add update benchmarks

* Add BenchmarkState.measure to only calculate the time to update

* Fix hang in update shallow benchmark

* Fix build errors

* Address build issues

* Remove File.swift headers

* Rename Element -> FiberElement and Element.Data -> FiberElement.Content

* Add doc comment explaining unowned usage

* Add doc comments explaining implicitly unwrapped optionals

* Attempt to use Swift instead of JS for applying mutations

* Fix issue with not applying updates to DOMFiberElement

* Add comment explaining manual implementation of Hashable for PropertyInfo

* Fix linter issues

* Remove dynamicMember label from subscript

* Re-enable carton test

* Attempt GTK fix

* Add option to disable layout in the FiberReconciler

* Re-enable TokamakDemo with StackReconciler

* Restore CI config

* Restore CI config

* Add file headers and cleanup structure

* Add 'px' to font-size in test outputs

* Remove extra newlines

* Keep track of 'elementChildren' so children are positioned in the correct order

* Use a ViewVisitor to pass the correct View type to the proposeSize function

* Add support for view modifiers

* Add frame modifier to demonstrate modifiers

* Fix TestRenderer

* Remove unused property

* Fix doc comment

* Fix linter issues and refactor slightly

* Fix benchmark builds

* Attempt to fix benchmarks

* Fix sibling layout issues

* Restore original demo

* Address review comments

* Remove maxAxis and fitAxis properties

* Use switch instead of ternary operators

* Add more documentation to layout steps

* Resolve reconciler issue due to alternate child not being cleared/released

* Apply suggestions from code review

Co-authored-by: Max Desiatov <max@desiatov.com>

* Reuse Text resolution code.

* Add more documentation

* Fix typo

* Use structs for LayoutComputers

* Update AlignmentID demo

* Fix weird formatting

Co-authored-by: Max Desiatov <max@desiatov.com>
  • Loading branch information
carson-katri and MaxDesiatov committed May 30, 2022
1 parent 355c880 commit 03513dd
Show file tree
Hide file tree
Showing 50 changed files with 1,416 additions and 302 deletions.
19 changes: 16 additions & 3 deletions Sources/TokamakCore/Environment/EnvironmentKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,24 @@ public protocol EnvironmentKey {
static var defaultValue: Value { get }
}

protocol EnvironmentModifier {
/// This protocol defines a type which mutates the environment in some way.
/// Unlike `EnvironmentalModifier`, which reads the environment to
/// create a `ViewModifier`.
///
/// It can be applied to a `View` or `ViewModifier`.
public protocol _EnvironmentModifier {
func modifyEnvironment(_ values: inout EnvironmentValues)
}

public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
public extension ViewModifier where Self: _EnvironmentModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
var environment = inputs.environment.environment
inputs.content.modifyEnvironment(&environment)
return .init(inputs: inputs, environment: environment)
}
}

public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentModifier {
public let keyPath: WritableKeyPath<EnvironmentValues, Value>
public let value: Value

Expand All @@ -32,7 +45,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo

public typealias Body = Never

func modifyEnvironment(_ values: inout EnvironmentValues) {
public func modifyEnvironment(_ values: inout EnvironmentValues) {
values[keyPath: keyPath] = value
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public extension EnvironmentValues {
}
}

struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
let environmentValues: EnvironmentValues

func body(content: Content) -> some View {
Expand Down
151 changes: 151 additions & 0 deletions Sources/TokamakCore/Fiber/AlignmentID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2022 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 2/18/22.
//

import Foundation

/// Used to identify an alignment guide.
///
/// Typically, you would define an alignment guide inside
/// an extension on `HorizontalAlignment` or `VerticalAlignment`:
///
/// extension HorizontalAlignment {
/// private enum MyAlignmentGuide: AlignmentID {
/// static func defaultValue(in context: ViewDimensions) -> CGFloat {
/// return 0.0
/// }
/// }
/// public static let myAlignmentGuide = Self(MyAlignmentGuide.self)
/// }
///
/// Which you can then use with the `alignmentGuide` modifier:
///
/// VStack(alignment: .myAlignmentGuide) {
/// Text("Align Leading")
/// .border(.red)
/// .alignmentGuide(.myAlignmentGuide) { $0[.leading] }
/// Text("Align Trailing")
/// .border(.blue)
/// .alignmentGuide(.myAlignmentGuide) { $0[.trailing] }
/// }
/// .border(.green)
public protocol AlignmentID {
/// The default value for this alignment guide
/// when not set via the `alignmentGuide` modifier.
static func defaultValue(in context: ViewDimensions) -> CGFloat
}

/// An alignment position along the horizontal axis.
@frozen public struct HorizontalAlignment: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}

let id: AlignmentID.Type

public init(_ id: AlignmentID.Type) {
self.id = id
}
}

extension HorizontalAlignment {
public static let leading = Self(Leading.self)

private enum Leading: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
}
}

public static let center = Self(Center.self)

private enum Center: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.width / 2
}
}

public static let trailing = Self(Trailing.self)

private enum Trailing: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.width
}
}
}

@frozen public struct VerticalAlignment: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}

let id: AlignmentID.Type

public init(_ id: AlignmentID.Type) {
self.id = id
}
}

extension VerticalAlignment {
public static let top = Self(Top.self)
private enum Top: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
}
}

public static let center = Self(Center.self)
private enum Center: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height / 2
}
}

public static let bottom = Self(Bottom.self)
private enum Bottom: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height
}
}

// TODO: Add baseline vertical alignment guides.
// public static let firstTextBaseline: VerticalAlignment
// public static let lastTextBaseline: VerticalAlignment
}

/// An alignment in both axes.
public struct Alignment: Equatable {
public var horizontal: HorizontalAlignment
public var vertical: VerticalAlignment

public init(
horizontal: HorizontalAlignment,
vertical: VerticalAlignment
) {
self.horizontal = horizontal
self.vertical = vertical
}

public static let topLeading = Self(horizontal: .leading, vertical: .top)
public static let top = Self(horizontal: .center, vertical: .top)
public static let topTrailing = Self(horizontal: .trailing, vertical: .top)
public static let leading = Self(horizontal: .leading, vertical: .center)
public static let center = Self(horizontal: .center, vertical: .center)
public static let trailing = Self(horizontal: .trailing, vertical: .center)
public static let bottomLeading = Self(horizontal: .leading, vertical: .bottom)
public static let bottom = Self(horizontal: .center, vertical: .bottom)
public static let bottomTrailing = Self(horizontal: .trailing, vertical: .bottom)
}
73 changes: 50 additions & 23 deletions Sources/TokamakCore/Fiber/Fiber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
// Created by Carson Katri on 2/15/22.
//

@_spi(TokamakCore) public extension FiberReconciler {
import Foundation

@_spi(TokamakCore)
public extension FiberReconciler {
/// A manager for a single `View`.
///
/// There are always 2 `Fiber`s for every `View` in the tree,
Expand Down Expand Up @@ -58,6 +61,8 @@
var id: Identity?
/// The mounted element, if this is a Renderer primitive.
var element: Renderer.ElementType?
/// The index of this element in its elementParent
var elementIndex: Int?
/// The first child node.
@_spi(TokamakCore) public var child: Fiber?
/// This node's right sibling.
Expand All @@ -75,6 +80,9 @@
/// Boxes that store `State` data.
var state: [PropertyInfo: MutableStorage] = [:]

/// The computed dimensions and origin.
var geometry: ViewGeometry?

/// The WIP node if this is current, or the current node if this is WIP.
weak var alternate: Fiber?

Expand Down Expand Up @@ -107,7 +115,7 @@
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
childIndex: Int,
elementIndex: Int?,
reconciler: FiberReconciler<Renderer>?
) {
self.reconciler = reconciler
Expand All @@ -117,14 +125,16 @@
self.elementParent = elementParent
typeInfo = TokamakCore.typeInfo(of: V.self)

let viewInputs = ViewInputs<V>(
view: view,
proposedSize: parent?.outputs.layoutComputer?.proposeSize(for: view, at: childIndex),
environment: parent?.outputs.environment ?? .init(.init())
)
state = bindProperties(to: &view, typeInfo, viewInputs)
let environment = parent?.outputs.environment ?? .init(.init())
state = bindProperties(to: &view, typeInfo, environment.environment)
self.view = view
outputs = V._makeView(viewInputs)
outputs = V._makeView(
.init(
content: view,
environment: environment
)
)

visitView = { [weak self] in
guard let self = self else { return }
// swiftlint:disable:next force_cast
Expand All @@ -134,7 +144,14 @@
if let element = element {
self.element = element
} else if Renderer.isPrimitive(view) {
self.element = .init(from: .init(from: view))
self.element = .init(
from: .init(from: view, shouldLayout: reconciler?.renderer.shouldLayout ?? false)
)
}

// Only specify an `elementIndex` if we have an element.
if self.element != nil {
self.elementIndex = elementIndex
}

let alternateView = view
Expand Down Expand Up @@ -198,7 +215,7 @@
private func bindProperties<V: View>(
to view: inout V,
_ typeInfo: TypeInfo?,
_ viewInputs: ViewInputs<V>
_ environment: EnvironmentValues
) -> [PropertyInfo: MutableStorage] {
guard let typeInfo = typeInfo else { return [:] }

Expand All @@ -215,7 +232,7 @@
storage.setter = { box.setValue($0, with: $1) }
value = storage
} else if var environmentReader = value as? EnvironmentReader {
environmentReader.setContent(from: viewInputs.environment.environment)
environmentReader.setContent(from: environment)
value = environmentReader
}
property.set(value: value, on: &view)
Expand All @@ -225,40 +242,50 @@

func update<V: View>(
with view: inout V,
childIndex: Int
elementIndex: Int?
) -> Renderer.ElementType.Content? {
typeInfo = TokamakCore.typeInfo(of: V.self)

let viewInputs = ViewInputs<V>(
view: view,
proposedSize: parent?.outputs.layoutComputer?.proposeSize(for: view, at: childIndex),
environment: parent?.outputs.environment ?? .init(.init())
)
state = bindProperties(to: &view, typeInfo, viewInputs)
self.elementIndex = elementIndex

let environment = parent?.outputs.environment ?? .init(.init())
state = bindProperties(to: &view, typeInfo, environment.environment)
self.view = view
outputs = V._makeView(viewInputs)
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)
return .init(from: view, shouldLayout: reconciler?.renderer.shouldLayout ?? false)
} else {
return nil
}
}

public var debugDescription: String {
flush()
if 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!))" : "") {
.split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {\(element != nil ?
"\n\(spaces)geometry: \(geometry)" :
"")
\(child?.flush(level: level + 2) ?? "")
\(spaces)}
\(sibling?.flush(level: level) ?? "")
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Fiber/FiberElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
init<V: View>(from primitiveView: V, shouldLayout: Bool)
}
Loading

0 comments on commit 03513dd

Please sign in to comment.