Skip to content

Commit

Permalink
Add _ShapeView and background modifiers support to Fiber renderers (
Browse files Browse the repository at this point in the history
#491)

* 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

* Support overriding visit function in renderer and _ShapeView drawing

* Support background modifier

* Resolve reconciler issues due to Optionals and elementIndex being set at wrong phase

* Remove Brewfile.lock.json

* Attempt to fix rendering tests

* Formatting nits

* Fix Gradient rendering

Co-authored-by: Max Desiatov <max@desiatov.com>
  • Loading branch information
carson-katri and MaxDesiatov authored Jun 15, 2022
1 parent 6e2ccf7 commit c935744
Show file tree
Hide file tree
Showing 22 changed files with 666 additions and 203 deletions.
5 changes: 5 additions & 0 deletions Sources/TokamakCore/Fiber/Fiber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ public extension FiberReconciler {
}
property.set(value: value, on: &content)
}
if var environmentReader = content as? EnvironmentReader {
environmentReader.setContent(from: environment)
// swiftlint:disable:next force_cast
content = environmentReader as! T
}
return state
}

Expand Down
25 changes: 7 additions & 18 deletions Sources/TokamakCore/Fiber/FiberReconciler+TreeReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension FiberReconciler {
update: { fiber, scene, _ in
fiber.update(with: &scene)
},
visitChildren: { $0._visitChildren }
visitChildren: { $1._visitChildren }
)
}

Expand All @@ -95,7 +95,9 @@ extension FiberReconciler {
update: { fiber, view, elementIndex in
fiber.update(with: &view, elementIndex: elementIndex)
},
visitChildren: { $0._visitChildren }
visitChildren: { reconciler, view in
reconciler?.renderer.viewVisitor(for: view) ?? view._visitChildren
}
)
}

Expand All @@ -105,7 +107,7 @@ extension FiberReconciler {
createFiber: (inout T, Renderer.ElementType?, Fiber?, Fiber?, Int?, FiberReconciler?)
-> Fiber,
update: (Fiber, inout T, Int?) -> Renderer.ElementType.Content?,
visitChildren: (T) -> (TreeReducer.SceneVisitor) -> ()
visitChildren: (FiberReconciler?, T) -> (TreeReducer.SceneVisitor) -> ()
) {
// Create the node and its element.
var nextValue = nextValue
Expand All @@ -125,7 +127,7 @@ extension FiberReconciler {
)
resultChild = Result(
fiber: existing,
visitChildren: visitChildren(nextValue),
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
parent: partialResult,
child: existing.child,
alternateChild: existing.alternate?.child,
Expand All @@ -134,13 +136,6 @@ extension FiberReconciler {
layoutContexts: partialResult.layoutContexts
)
partialResult.nextExisting = existing.sibling

// If this fiber has an element, increment the elementIndex for its parent.
if let key = key,
existing.element != nil
{
partialResult.elementIndices[key] = partialResult.elementIndices[key, default: 0] + 1
}
} else {
let elementParent = partialResult.fiber?.element != nil
? partialResult.fiber
Expand All @@ -165,15 +160,9 @@ extension FiberReconciler {
fiber.alternate = alternate
partialResult.nextExistingAlternate = alternate.sibling
}
// If this fiber has an element, increment the elementIndex for its parent.
if let key = key,
fiber.element != nil
{
partialResult.elementIndices[key] = partialResult.elementIndices[key, default: 0] + 1
}
resultChild = Result(
fiber: fiber,
visitChildren: visitChildren(nextValue),
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
parent: partialResult,
child: nil,
alternateChild: fiber.alternate?.child,
Expand Down
17 changes: 13 additions & 4 deletions Sources/TokamakCore/Fiber/FiberReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
}

func visit<V>(_ view: V) where V: View {
visitAny(view, visitChildren: view._visitChildren)
visitAny(view, visitChildren: reconciler.renderer.viewVisitor(for: view))
}

func visit<S>(_ scene: S) where S: Scene {
Expand Down Expand Up @@ -224,8 +224,7 @@ 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
{
Expand Down Expand Up @@ -327,15 +326,25 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
/// The main reconciler loop.
func mainLoop() {
while true {
// If this fiber has an element, set its `elementIndex`
// and increment the `elementIndices` value for its `elementParent`.
if node.fiber?.element != nil,
let elementParent = node.fiber?.elementParent
{
let key = ObjectIdentifier(elementParent)
node.fiber?.elementIndex = elementIndices[key, default: 0]
elementIndices[key] = elementIndices[key, default: 0] + 1
}

// Perform work on the node.
reconcile(node)

// Ensure the TreeReducer can access the `elementIndices`.
node.elementIndices = elementIndices

// Compute the children of the 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.useDynamicLayout,
Expand Down
17 changes: 17 additions & 0 deletions Sources/TokamakCore/Fiber/FiberRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public protocol FiberRenderer {
associatedtype ElementType: FiberElement
/// Check whether a `View` is a primitive for this renderer.
static func isPrimitive<V>(_ view: V) -> Bool where V: View
func visitPrimitiveChildren<Primitive, Visitor>(
_ view: Primitive
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor
/// Apply the mutations to the elements.
func commit(_ mutations: [Mutation<Self>])
/// The root element all top level views should be mounted on.
Expand All @@ -40,6 +43,20 @@ public protocol FiberRenderer {
public extension FiberRenderer {
var defaultEnvironment: EnvironmentValues { .init() }

func visitPrimitiveChildren<Primitive, Visitor>(
_ view: Primitive
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor {
nil
}

func viewVisitor<V: View, Visitor: ViewVisitor>(for view: V) -> ViewVisitorF<Visitor> {
if Self.isPrimitive(view) {
return visitPrimitiveChildren(view) ?? view._visitChildren
} else {
return view._visitChildren
}
}

@discardableResult
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
.init(self, view)
Expand Down
85 changes: 85 additions & 0 deletions Sources/TokamakCore/Fiber/Layout/BackgroundLayoutComputer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2021 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/24/22.
//

import Foundation

/// A `LayoutComputer` that constrains a background to a foreground.
final class BackgroundLayoutComputer: LayoutComputer {
let proposedSize: CGSize
let alignment: Alignment

init(proposedSize: CGSize, alignment: Alignment) {
self.proposedSize = proposedSize
self.alignment = alignment
}

func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
where V: View
{
if index == 0 {
// The foreground can pick their size.
return proposedSize
} else {
// The background is constrained to the foreground.
return context.children.first?.dimensions.size ?? .zero
}
}

func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
let foregroundSize = ViewDimensions(
size: .init(
width: context.children.first?.dimensions.width ?? 0,
height: context.children.first?.dimensions.height ?? 0
),
alignmentGuides: [:]
)
return .init(
x: foregroundSize[alignment.horizontal] - child.dimensions[alignment.horizontal],
y: foregroundSize[alignment.vertical] - child.dimensions[alignment.vertical]
)
}

func requestSize(in context: LayoutContext) -> CGSize {
let childSize = context.children.reduce(CGSize.zero) {
.init(
width: max($0.width, $1.dimensions.width),
height: max($0.height, $1.dimensions.height)
)
}
return .init(width: childSize.width, height: childSize.height)
}
}

public extension _BackgroundLayout {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
layoutComputer: {
BackgroundLayoutComputer(proposedSize: $0, alignment: inputs.content.alignment)
}
)
}
}

public extension _BackgroundStyleModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
layoutComputer: { BackgroundLayoutComputer(proposedSize: $0, alignment: .center) }
)
}
}
11 changes: 6 additions & 5 deletions Sources/TokamakCore/Fiber/Layout/FlexLayoutComputer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,25 @@
import Foundation

/// A `LayoutComputer` that fills its parent.
struct FlexLayoutComputer: LayoutComputer {
@_spi(TokamakCore)
public struct FlexLayoutComputer: LayoutComputer {
let proposedSize: CGSize

init(proposedSize: CGSize) {
public init(proposedSize: CGSize) {
self.proposedSize = proposedSize
}

func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
public func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
where V: View
{
proposedSize
}

func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
public func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
.zero
}

func requestSize(in context: LayoutContext) -> CGSize {
public func requestSize(in context: LayoutContext) -> CGSize {
proposedSize
}
}
11 changes: 6 additions & 5 deletions Sources/TokamakCore/Fiber/Layout/FrameLayoutComputer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,27 @@
import Foundation

/// A `LayoutComputer` that uses a specified size in one or more axes.
struct FrameLayoutComputer: LayoutComputer {
@_spi(TokamakCore)
public struct FrameLayoutComputer: LayoutComputer {
let proposedSize: CGSize
let width: CGFloat?
let height: CGFloat?
let alignment: Alignment

init(proposedSize: CGSize, width: CGFloat?, height: CGFloat?, alignment: Alignment) {
public init(proposedSize: CGSize, width: CGFloat?, height: CGFloat?, alignment: Alignment) {
self.proposedSize = proposedSize
self.width = width
self.height = height
self.alignment = alignment
}

func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
public func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
where V: View
{
.init(width: width ?? proposedSize.width, height: height ?? proposedSize.height)
}

func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
public func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
let size = ViewDimensions(
size: .init(
width: width ?? child.dimensions.width,
Expand All @@ -51,7 +52,7 @@ struct FrameLayoutComputer: LayoutComputer {
)
}

func requestSize(in context: LayoutContext) -> CGSize {
public func requestSize(in context: LayoutContext) -> CGSize {
let childSize = context.children.reduce(CGSize.zero) {
.init(
width: max($0.width, $1.dimensions.width),
Expand Down
11 changes: 6 additions & 5 deletions Sources/TokamakCore/Fiber/Layout/ShrinkWrapLayoutComputer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,25 @@
import Foundation

/// A `LayoutComputer` that shrinks to the size of its children.
struct ShrinkWrapLayoutComputer: LayoutComputer {
@_spi(TokamakCore)
public struct ShrinkWrapLayoutComputer: LayoutComputer {
let proposedSize: CGSize

init(proposedSize: CGSize) {
public init(proposedSize: CGSize) {
self.proposedSize = proposedSize
}

func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
public func proposeSize<V>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
where V: View
{
proposedSize
}

func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
public func position(_ child: LayoutContext.Child, in context: LayoutContext) -> CGPoint {
.zero
}

func requestSize(in context: LayoutContext) -> CGSize {
public func requestSize(in context: LayoutContext) -> CGSize {
context.children.reduce(CGSize.zero) {
.init(
width: max($0.width, $1.dimensions.width),
Expand Down
12 changes: 6 additions & 6 deletions Sources/TokamakCore/Fiber/LayoutComputer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
import Foundation

/// The currently computed children.
struct LayoutContext {
var children: [Child]
public struct LayoutContext {
public var children: [Child]

struct Child {
let index: Int
let dimensions: ViewDimensions
public struct Child {
public let index: Int
public let dimensions: ViewDimensions
}
}

Expand All @@ -40,7 +40,7 @@ struct LayoutContext {
/// The same `LayoutComputer` instance will be used for any given view during a single layout pass.
///
/// Sizes from `proposeSize` will be clamped, so it is safe to return negative numbers.
protocol LayoutComputer {
public protocol LayoutComputer {
/// Will be called every time a child is evaluated.
/// The calls will always be in order, and no more than one call will be made per child.
func proposeSize<V: View>(for child: V, at index: Int, in context: LayoutContext) -> CGSize
Expand Down
Loading

0 comments on commit c935744

Please sign in to comment.