Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Layout Engine for Fiber Reconciler #472

Merged
merged 73 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
84018db
Initial Reconciler using visitor pattern
carson-katri Feb 6, 2022
aa8c4dd
Preliminary static HTML renderer using the new reconciler
carson-katri Feb 6, 2022
67b3509
Add environment
carson-katri Feb 7, 2022
7f96bc1
Initial DOM renderer
carson-katri Feb 7, 2022
4ea2247
Nearly-working and simplified reconciler
carson-katri Feb 11, 2022
026f00b
Working reconciler for HTML/DOM renderers
carson-katri Feb 15, 2022
e340ed5
Rename files, and split code across files
carson-katri Feb 15, 2022
885ba06
Add some documentation and refinements
carson-katri Feb 16, 2022
dd8a2eb
Remove GraphRendererTests
carson-katri Feb 16, 2022
88f689f
Initial layout engine (only implemented for the TestRenderer)
carson-katri Feb 19, 2022
a9af988
Layout engine for the DOM renderer
carson-katri Feb 21, 2022
26ce229
Refined layout pass
carson-katri Mar 3, 2022
2891e6b
Revise positioning and restoration of position styles on .update
carson-katri Mar 3, 2022
055c826
Re-add Optional.body for StackReconciler-based renderers
carson-katri Mar 3, 2022
7b98447
Merge branch 'fiber/core' of https://github.com/TokamakUI/Tokamak int…
carson-katri Mar 7, 2022
9902ace
Add text measurement
carson-katri Mar 7, 2022
d576798
Add spacing to StackLayout
carson-katri Mar 7, 2022
c457dce
Add benchmarks to compare the stack/fiber reconcilers
carson-katri Apr 5, 2022
be6aa89
Fix some issues created for the StackReconciler, and add update bench…
carson-katri Apr 5, 2022
ac5c110
Add BenchmarkState.measure to only calculate the time to update
carson-katri Apr 6, 2022
0d82cec
Fix hang in update shallow benchmark
carson-katri Apr 6, 2022
42c7c01
Merge branch 'main' into fiber/core
MaxDesiatov Apr 10, 2022
7ef9bed
Fix build errors
MaxDesiatov Apr 10, 2022
34cd9d7
Address build issues
MaxDesiatov Apr 10, 2022
6a846f4
Merge branch 'main' into fiber/core
MaxDesiatov May 12, 2022
7d705eb
Merge branch 'main' of https://github.com/TokamakUI/Tokamak into fibe…
carson-katri May 19, 2022
a70a938
Remove File.swift headers
carson-katri May 19, 2022
485da76
Rename Element -> FiberElement and Element.Data -> FiberElement.Content
carson-katri May 20, 2022
81deb5a
Add doc comment explaining unowned usage
carson-katri May 20, 2022
74ee853
Add doc comments explaining implicitly unwrapped optionals
carson-katri May 21, 2022
8a8c075
Attempt to use Swift instead of JS for applying mutations
carson-katri May 22, 2022
b7cc779
Fix issue with not applying updates to DOMFiberElement
carson-katri May 22, 2022
78d0bf5
Add comment explaining manual implementation of Hashable for Property…
carson-katri May 22, 2022
4becea3
Fix linter issues
carson-katri May 22, 2022
8c9141b
Remove dynamicMember label from subscript
carson-katri May 23, 2022
33f0e67
Re-enable carton test
carson-katri May 23, 2022
316468c
Merge fiber/core
carson-katri May 23, 2022
4d86f89
Attempt GTK fix
carson-katri May 23, 2022
3189e77
Add option to disable layout in the FiberReconciler
carson-katri May 23, 2022
104f14f
Re-enable TokamakDemo with StackReconciler
carson-katri May 23, 2022
0d0a9c7
Merge fiber/core
carson-katri May 24, 2022
836ac36
Merge main (take ours)
carson-katri May 24, 2022
389b3e8
Restore CI config
carson-katri May 24, 2022
a20d94b
Restore CI config
carson-katri May 24, 2022
05d5a24
Add file headers and cleanup structure
carson-katri May 24, 2022
f628cba
Merge branch 'main' of github.com:tokamakui/tokamak into fiber/layout
carson-katri May 26, 2022
d507848
Add 'px' to font-size in test outputs
carson-katri May 26, 2022
65e78e8
Remove extra newlines
carson-katri May 26, 2022
f94d733
Keep track of 'elementChildren' so children are positioned in the cor…
carson-katri May 27, 2022
001cd4b
Use a ViewVisitor to pass the correct View type to the proposeSize fu…
carson-katri May 27, 2022
f89d24d
Add support for view modifiers
carson-katri May 28, 2022
10368b1
Add frame modifier to demonstrate modifiers
carson-katri May 28, 2022
f926fbe
Fix TestRenderer
carson-katri May 28, 2022
4d96217
Remove unused property
carson-katri May 28, 2022
aeb5983
Fix doc comment
carson-katri May 28, 2022
3265b74
Fix linter issues and refactor slightly
carson-katri May 28, 2022
b6c5d8e
Fix benchmark builds
carson-katri May 28, 2022
8ca135c
Attempt to fix benchmarks
carson-katri May 28, 2022
8779df6
Fix sibling layout issues
carson-katri May 28, 2022
90e1c5d
Restore original demo
carson-katri May 28, 2022
0cf20a0
Address review comments
carson-katri May 29, 2022
8a96570
Remove maxAxis and fitAxis properties
carson-katri May 30, 2022
ca7fb6b
Use switch instead of ternary operators
carson-katri May 30, 2022
e90f1a1
Add more documentation to layout steps
carson-katri May 30, 2022
a0b71d3
Resolve reconciler issue due to alternate child not being cleared/rel…
carson-katri May 30, 2022
8cd570a
Apply suggestions from code review
carson-katri May 30, 2022
c0567d4
Reuse Text resolution code.
carson-katri May 30, 2022
7a56023
Merge branch 'fiber/layout' of github.com:tokamakui/tokamak into fibe…
carson-katri May 30, 2022
e9671c3
Add more documentation
carson-katri May 30, 2022
3c92483
Fix typo
carson-katri May 30, 2022
cfaf990
Use structs for LayoutComputers
carson-katri May 30, 2022
cff213d
Update AlignmentID demo
carson-katri May 30, 2022
cdb5df2
Fix weird formatting
carson-katri May 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
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 {
carson-katri marked this conversation as resolved.
Show resolved Hide resolved
/// 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))
carson-katri marked this conversation as resolved.
Show resolved Hide resolved
}

// Only specify an elementIndex if we have an element.
carson-katri marked this conversation as resolved.
Show resolved Hide resolved
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