From 9c1d61b7a52586652b6a28e1ee2060bd03653ded Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 21 Jul 2020 11:09:20 -0400 Subject: [PATCH 01/10] Initial TokamakStatic implementation --- Package.swift | 26 +++- .../Environment/EnvironmentObject.swift | 4 + .../Environment/EnvironmentValues.swift | 4 + .../MountedViews/MountedCompositeView.swift | 4 + Sources/TokamakCore/ObservedObject.swift | 7 +- .../TokamakDOM/Resources/TokamakStyles.swift | 2 +- Sources/TokamakDOM/Styles/ToggleStyle.swift | 2 + Sources/TokamakDOM/Views/Divider.swift | 8 +- Sources/TokamakDOM/Views/HTML.swift | 10 +- Sources/TokamakDOM/Views/Text.swift | 6 +- Sources/TokamakStatic/Core.swift | 87 ++++++++++++++ Sources/TokamakStatic/StaticRenderer.swift | 111 ++++++++++++++++++ Sources/TokamakStaticDemo/main.swift | 17 +++ 13 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 Sources/TokamakStatic/Core.swift create mode 100644 Sources/TokamakStatic/StaticRenderer.swift create mode 100644 Sources/TokamakStaticDemo/main.swift diff --git a/Package.swift b/Package.swift index 173cca8b1..54fc312b4 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,14 @@ let package = Package( name: "TokamakShim", targets: ["TokamakShim"] ), + .library( + name: "TokamakStatic", + targets: ["TokamakStatic"] + ), + .executable( + name: "TokamakStaticDemo", + targets: ["TokamakStaticDemo"] + ), ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -40,7 +48,10 @@ let package = Package( // in packages which this package depends on. .target( name: "TokamakCore", - dependencies: ["OpenCombine", "Runtime"] + dependencies: [ + .byName(name: "OpenCombine", condition: .when(platforms: [.wasi])), + "Runtime", + ] ), .target( name: "TokamakDemo", @@ -54,6 +65,19 @@ let package = Package( name: "TokamakShim", dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))] ), + .target( + name: "TokamakStatic", + dependencies: [ + "TokamakCore", + "TokamakDOM", + ] + ), + .target( + name: "TokamakStaticDemo", + dependencies: [ + "TokamakStatic", + ] + ), .target( name: "TokamakTestRenderer", dependencies: ["TokamakCore"] diff --git a/Sources/TokamakCore/Environment/EnvironmentObject.swift b/Sources/TokamakCore/Environment/EnvironmentObject.swift index 46edc7961..ed21cc39b 100644 --- a/Sources/TokamakCore/Environment/EnvironmentObject.swift +++ b/Sources/TokamakCore/Environment/EnvironmentObject.swift @@ -15,7 +15,11 @@ // Created by Carson Katri on 7/7/20. // +#if os(WASI) import OpenCombine +#else +import Combine +#endif @propertyWrapper public struct EnvironmentObject: ObservedProperty, EnvironmentReader diff --git a/Sources/TokamakCore/Environment/EnvironmentValues.swift b/Sources/TokamakCore/Environment/EnvironmentValues.swift index 4f0f011e4..880aa4602 100644 --- a/Sources/TokamakCore/Environment/EnvironmentValues.swift +++ b/Sources/TokamakCore/Environment/EnvironmentValues.swift @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if os(WASI) import OpenCombine +#else +import Combine +#endif public struct EnvironmentValues: CustomStringConvertible { public var description: String { diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index 0d5bc66a5..af29d7b81 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -15,7 +15,11 @@ // Created by Max Desiatov on 03/12/2018. // +#if os(WASI) import OpenCombine +#else +import Combine +#endif import Runtime final class MountedCompositeView: MountedView, Hashable { diff --git a/Sources/TokamakCore/ObservedObject.swift b/Sources/TokamakCore/ObservedObject.swift index 66187ba9f..a0c7b3105 100644 --- a/Sources/TokamakCore/ObservedObject.swift +++ b/Sources/TokamakCore/ObservedObject.swift @@ -12,10 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if os(WASI) import OpenCombine - public typealias ObservableObject = OpenCombine.ObservableObject public typealias Published = OpenCombine.Published +#else +import Combine +public typealias ObservableObject = Combine.ObservableObject +public typealias Published = Combine.Published +#endif protocol ObservedProperty { var objectWillChange: AnyPublisher<(), Never> { get } diff --git a/Sources/TokamakDOM/Resources/TokamakStyles.swift b/Sources/TokamakDOM/Resources/TokamakStyles.swift index bea4c6db3..acac10281 100644 --- a/Sources/TokamakDOM/Resources/TokamakStyles.swift +++ b/Sources/TokamakDOM/Resources/TokamakStyles.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -let tokamakStyles = """ +public let tokamakStyles = """ ._tokamak-stack > * { flex-shrink: 0; } diff --git a/Sources/TokamakDOM/Styles/ToggleStyle.swift b/Sources/TokamakDOM/Styles/ToggleStyle.swift index b2bd1b73f..945683ab3 100644 --- a/Sources/TokamakDOM/Styles/ToggleStyle.swift +++ b/Sources/TokamakDOM/Styles/ToggleStyle.swift @@ -21,6 +21,8 @@ public struct DefaultToggleStyle: ToggleStyle { public func makeBody(configuration: Configuration) -> some View { CheckboxToggleStyle().makeBody(configuration: configuration) } + + public init() {} } public struct CheckboxToggleStyle: ToggleStyle { diff --git a/Sources/TokamakDOM/Views/Divider.swift b/Sources/TokamakDOM/Views/Divider.swift index 8dc959b7a..a04d5846f 100644 --- a/Sources/TokamakDOM/Views/Divider.swift +++ b/Sources/TokamakDOM/Views/Divider.swift @@ -15,9 +15,9 @@ import TokamakCore extension Divider: AnyHTML { - var innerHTML: String? { nil } - var tag: String { "hr" } - var attributes: [String: String] { + public var innerHTML: String? { nil } + public var tag: String { "hr" } + public var attributes: [String: String] { [ "style": """ width: 100%; height: 0; margin: 0; @@ -29,5 +29,5 @@ extension Divider: AnyHTML { ] } - var listeners: [String: Listener] { [:] } + public var listeners: [String: Listener] { [:] } } diff --git a/Sources/TokamakDOM/Views/HTML.swift b/Sources/TokamakDOM/Views/HTML.swift index 366cea43d..2847ccbc6 100644 --- a/Sources/TokamakDOM/Views/HTML.swift +++ b/Sources/TokamakDOM/Views/HTML.swift @@ -20,7 +20,7 @@ import TokamakCore public typealias Listener = (JSObjectRef) -> () -protocol AnyHTML { +public protocol AnyHTML { var innerHTML: String? { get } var tag: String { get } var attributes: [String: String] { get } @@ -56,9 +56,9 @@ extension AnyHTML { } public struct HTML: View, AnyHTML where Content: View { - let tag: String - let attributes: [String: String] - let listeners: [String: Listener] + public let tag: String + public let attributes: [String: String] + public let listeners: [String: Listener] let content: Content public init( @@ -73,7 +73,7 @@ public struct HTML: View, AnyHTML where Content: View { self.content = content() } - var innerHTML: String? { nil } + public var innerHTML: String? { nil } public var body: Never { neverBody("HTML") diff --git a/Sources/TokamakDOM/Views/Text.swift b/Sources/TokamakDOM/Views/Text.swift index 5eba49244..13e2af94c 100644 --- a/Sources/TokamakDOM/Views/Text.swift +++ b/Sources/TokamakDOM/Views/Text.swift @@ -102,8 +102,8 @@ extension Text: AnyHTML { } } - var tag: String { "span" } - var attributes: [String: String] { + public var tag: String { "span" } + public var attributes: [String: String] { var font: Font? var color: Color? var italic: Bool = false @@ -160,5 +160,5 @@ extension Text: AnyHTML { ] } - var listeners: [String: Listener] { [:] } + public var listeners: [String: Listener] { [:] } } diff --git a/Sources/TokamakStatic/Core.swift b/Sources/TokamakStatic/Core.swift new file mode 100644 index 000000000..e0482f770 --- /dev/null +++ b/Sources/TokamakStatic/Core.swift @@ -0,0 +1,87 @@ +// Copyright 2020 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 7/20/20. +// + +import TokamakCore + +// MARK: Environment & State + +public typealias Environment = TokamakCore.Environment + +// MARK: Modifiers & Styles + +public typealias ViewModifier = TokamakCore.ViewModifier +public typealias ModifiedContent = TokamakCore.ModifiedContent + +public typealias DefaultListStyle = TokamakCore.DefaultListStyle +public typealias PlainListStyle = TokamakCore.PlainListStyle +public typealias InsetListStyle = TokamakCore.InsetListStyle +public typealias GroupedListStyle = TokamakCore.GroupedListStyle +public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle + +// MARK: Shapes + +public typealias Shape = TokamakCore.Shape + +public typealias Capsule = TokamakCore.Capsule +public typealias Circle = TokamakCore.Circle +public typealias Ellipse = TokamakCore.Ellipse +public typealias Path = TokamakCore.Path +public typealias Rectangle = TokamakCore.Rectangle +public typealias RoundedRectangle = TokamakCore.RoundedRectangle + +// MARK: Primitive values + +public typealias Color = TokamakCore.Color +public typealias Font = TokamakCore.Font + +public typealias CGAffineTransform = TokamakCore.CGAffineTransform +public typealias CGPoint = TokamakCore.CGPoint +public typealias CGRect = TokamakCore.CGRect +public typealias CGSize = TokamakCore.CGSize + +// MARK: Views + +public typealias Divider = TokamakCore.Divider +public typealias ForEach = TokamakCore.ForEach +public typealias GridItem = TokamakCore.GridItem +public typealias Group = TokamakCore.Group +public typealias HStack = TokamakCore.HStack +public typealias LazyHGrid = TokamakCore.LazyHGrid +public typealias LazyVGrid = TokamakCore.LazyVGrid +public typealias List = TokamakCore.List +public typealias ScrollView = TokamakCore.ScrollView +public typealias Section = TokamakCore.Section +public typealias Spacer = TokamakCore.Spacer +public typealias Text = TokamakCore.Text +public typealias VStack = TokamakCore.VStack +public typealias ZStack = TokamakCore.ZStack + +// MARK: Special Views + +public typealias View = TokamakCore.View +public typealias AnyView = TokamakCore.AnyView +public typealias EmptyView = TokamakCore.EmptyView + +// MARK: Misc + +// FIXME: I would put this inside TokamakCore, but for +// some reason it doesn't get exported with the typealias +extension Text { + public static func + (lhs: Self, rhs: Self) -> Self { + _concatenating(lhs: lhs, rhs: rhs) + } +} diff --git a/Sources/TokamakStatic/StaticRenderer.swift b/Sources/TokamakStatic/StaticRenderer.swift new file mode 100644 index 000000000..fbb572e28 --- /dev/null +++ b/Sources/TokamakStatic/StaticRenderer.swift @@ -0,0 +1,111 @@ +// Copyright 2020 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 7/20/20. +// + +import TokamakCore +import TokamakDOM + +public final class HTMLTarget: Target { + var html: AnyHTML + var children: [HTMLTarget] = [] + + init(_ view: V, + _ html: AnyHTML) { + self.html = html + super.init(view) + } +} + +extension HTMLTarget { + var outerHTML: String { + """ + <\(html.tag)\(html.attributes.isEmpty ? "" : " ")\ + \(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\ + \(html.innerHTML ?? "")\ + \(children.map(\.outerHTML).joined(separator: "\n"))\ + + """ + } +} + +struct HTMLBody: AnyHTML { + let tag: String = "body" + let innerHTML: String? = nil + let attributes: [String: String] = [:] + let listeners: [String: Listener] = [:] +} + +public final class StaticRenderer: Renderer { + public private(set) var reconciler: StackReconciler? + + var rootTarget: HTMLTarget + + public var html: String { + """ + + + + + \(rootTarget.outerHTML) + + """ + } + + public init(_ view: V) { + rootTarget = HTMLTarget(view, HTMLBody()) + reconciler = StackReconciler( + view: view, + target: rootTarget, + renderer: self, + environment: EnvironmentValues() + ) { _ in + fatalError("Stateful apps cannot be created with TokamakStatic") + } + } + + public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { + guard let html = mapAnyView( + host.view, + transform: { (html: AnyHTML) in html } + ) else { + // handle cases like `TupleView` + if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { + return parent + } + + return nil + } + + let node = HTMLTarget(host.view, html) + parent.children.append(node) + return node + } + + public func update(target: HTMLTarget, with host: MountedHost) { + fatalError("Stateful apps cannot be created with TokamakStatic") + } + + public func unmount( + target: HTMLTarget, + from parent: HTMLTarget, + with host: MountedHost, + completion: @escaping () -> () + ) { + fatalError("Stateful apps cannot be created with TokamakStatic") + } +} diff --git a/Sources/TokamakStaticDemo/main.swift b/Sources/TokamakStaticDemo/main.swift new file mode 100644 index 000000000..c37373acb --- /dev/null +++ b/Sources/TokamakStaticDemo/main.swift @@ -0,0 +1,17 @@ +// +// File.swift +// +// +// Created by Carson Katri on 7/20/20. +// + +import TokamakStatic + +struct ContentView: View { + var body: some View { + Text("Hello, world!") + } +} + +let renderer = StaticRenderer(ContentView()) +print(renderer.html) From b21f6eaafa4b0ba2baf34b3e6938884684020a3c Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Wed, 22 Jul 2020 16:45:18 -0400 Subject: [PATCH 02/10] Add docs to 'docs' folder --- .../1 Renderers in Tokamak.md | 15 +++ .../2 Understanding Renderers.md | 12 ++ .../3 TokamakStatic Setup.md | 79 +++++++++++ .../4 Building the Target.md | 44 +++++++ .../5 Building the Renderer.md | 124 ++++++++++++++++++ ... Providing platform-specific primitives.md | 54 ++++++++ 6 files changed, 328 insertions(+) create mode 100644 docs/Building a Renderer/1 Renderers in Tokamak.md create mode 100644 docs/Building a Renderer/2 Understanding Renderers.md create mode 100644 docs/Building a Renderer/3 TokamakStatic Setup.md create mode 100644 docs/Building a Renderer/4 Building the Target.md create mode 100644 docs/Building a Renderer/5 Building the Renderer.md create mode 100644 docs/Building a Renderer/6 Providing platform-specific primitives.md diff --git a/docs/Building a Renderer/1 Renderers in Tokamak.md b/docs/Building a Renderer/1 Renderers in Tokamak.md new file mode 100644 index 000000000..73e8512fd --- /dev/null +++ b/docs/Building a Renderer/1 Renderers in Tokamak.md @@ -0,0 +1,15 @@ +# `Renderers` in Tokamak +Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use to construct a representation of `Views` that your platform understands. + +To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStatic` (which you can find in the `Tokamak` repository). + +Before we create the `Renderer`, we need to understand the requirements of our platform: + +1. Stateful apps cannot be created + This simplifies the scope of our project, as we only have to render once. However, if you are building a `Renderer` that supports state changes, the process is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a `View` has to be redrawn. +2. HTML should be rendered + `TokamakDOM` provides HTML representations of many `Views`, so we can utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can understand, and when you are required to do so. + +And that’s it! In the next part we’ll go more in depth on `Renderers`. + +#tokamak diff --git a/docs/Building a Renderer/2 Understanding Renderers.md b/docs/Building a Renderer/2 Understanding Renderers.md new file mode 100644 index 000000000..e62767e88 --- /dev/null +++ b/docs/Building a Renderer/2 Understanding Renderers.md @@ -0,0 +1,12 @@ +# Understanding `Renderers` +So, what goes into a `Renderer`? + +1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is `UIView`, on macOS an `NSView`, and on the web we render to DOM nodes. +2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It notifies your `Renderer` of what views need to be mounted/unmounted. +3. `func mountTarget`- This function is called when a new target instance should be created and added to the parent (either as a subview or some other way, e.g. installed if it’s a layout constraint). +4. `func update` - This function is called when an existing target instance should be updated (e.g. when `State` changes). +5. `func unmount` - This function is called when an existing target instance should be unmounted: removed from the parent and most likely destroyed. + +That’s it! Let’s get our project setup. + +#tokamak diff --git a/docs/Building a Renderer/3 TokamakStatic Setup.md b/docs/Building a Renderer/3 TokamakStatic Setup.md new file mode 100644 index 000000000..678d2ee27 --- /dev/null +++ b/docs/Building a Renderer/3 TokamakStatic Setup.md @@ -0,0 +1,79 @@ +# `TokamakStatic` Setup +Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to use. A `Core.swift` file is used to reexport these symbols. For `TokamakStatic`, we’ll use the following `Core.swift` file: + +```swift +import TokamakCore + +// MARK: Environment & State + +public typealias Environment = TokamakCore.Environment + +// MARK: Modifiers & Styles + +public typealias ViewModifier = TokamakCore.ViewModifier +public typealias ModifiedContent = TokamakCore.ModifiedContent + +public typealias DefaultListStyle = TokamakCore.DefaultListStyle +public typealias PlainListStyle = TokamakCore.PlainListStyle +public typealias InsetListStyle = TokamakCore.InsetListStyle +public typealias GroupedListStyle = TokamakCore.GroupedListStyle +public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle + +// MARK: Shapes + +public typealias Shape = TokamakCore.Shape + +public typealias Capsule = TokamakCore.Capsule +public typealias Circle = TokamakCore.Circle +public typealias Ellipse = TokamakCore.Ellipse +public typealias Path = TokamakCore.Path +public typealias Rectangle = TokamakCore.Rectangle +public typealias RoundedRectangle = TokamakCore.RoundedRectangle + +// MARK: Primitive values + +public typealias Color = TokamakCore.Color +public typealias Font = TokamakCore.Font + +public typealias CGAffineTransform = TokamakCore.CGAffineTransform +public typealias CGPoint = TokamakCore.CGPoint +public typealias CGRect = TokamakCore.CGRect +public typealias CGSize = TokamakCore.CGSize + +// MARK: Views + +public typealias Divider = TokamakCore.Divider +public typealias ForEach = TokamakCore.ForEach +public typealias GridItem = TokamakCore.GridItem +public typealias Group = TokamakCore.Group +public typealias HStack = TokamakCore.HStack +public typealias LazyHGrid = TokamakCore.LazyHGrid +public typealias LazyVGrid = TokamakCore.LazyVGrid +public typealias List = TokamakCore.List +public typealias ScrollView = TokamakCore.ScrollView +public typealias Section = TokamakCore.Section +public typealias Spacer = TokamakCore.Spacer +public typealias Text = TokamakCore.Text +public typealias VStack = TokamakCore.VStack +public typealias ZStack = TokamakCore.ZStack + +// MARK: Special Views + +public typealias View = TokamakCore.View +public typealias AnyView = TokamakCore.AnyView +public typealias EmptyView = TokamakCore.EmptyView + +// MARK: Misc + +// Note: This extension is required to support concatenation of `Text`. +extension Text { + public static func + (lhs: Self, rhs: Self) -> Self { + _concatenating(lhs: lhs, rhs: rhs) + } +} + +``` + +We’ve omitted any stateful `Views`, as well as property wrappers used to modify state. + +#tokamak diff --git a/docs/Building a Renderer/4 Building the Target.md b/docs/Building a Renderer/4 Building the Target.md new file mode 100644 index 000000000..fb0f442e6 --- /dev/null +++ b/docs/Building a Renderer/4 Building the Target.md @@ -0,0 +1,44 @@ +# Building the `Target` +If you recall, we defined a `Target` as: + +> the destination for rendered `Views` + +In `TokamakStatic`, this would be a tag in an `HTML` file. A tag has several properties, although we don’t need to worry about all of them. For now, we can consider a tag to have: + +* The HTML for the tag itself (outer HTML) +* Child tags (inner HTML) + +We can describe our target simply: + +```swift +public final class HTMLTarget: Target { + var html: AnyHTML + var children: [HTMLTarget] = [] + + init(_ view: V, + _ html: AnyHTML) { + self.html = html + super.init(view) + } +} +``` + +`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View` it hosts, the `HTML` that represents it, and its child elements. + +Lastly, we can also provide an HTML string representation of the target: + +```swift +extension HTMLTarget { + var outerHTML: String { + """ + <\(html.tag)\(html.attributes.isEmpty ? "" : " ")\ + \(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\ + \(html.innerHTML ?? "")\ + \(children.map(\.outerHTML).joined(separator: "\n"))\ + + """ + } +} +``` + +#tokamak diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md new file mode 100644 index 000000000..c32db1b17 --- /dev/null +++ b/docs/Building a Renderer/5 Building the Renderer.md @@ -0,0 +1,124 @@ +# Building the `Renderer` +Now that we have a `Target`, we can start the `Renderer`: + +```swift +public final class StaticRenderer: Renderer { + public private(set) var reconciler: StackReconciler? + var rootTarget: HTMLTarget + + public var html: String { + """ + + \(rootTarget.outerHTML) + + """ + } +} +``` + +We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus on mounting and un-mounting `Views`. + +```swift +... +public init(_ view: V) { + rootTarget = HTMLTarget(view, HTMLBody()) + reconciler = StackReconciler( + view: view, + target: rootTarget, + renderer: self, + environment: EnvironmentValues() + ) { closure in + fatalError("Stateful apps cannot be created with TokamakStatic") + } +} +``` + +Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the `View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default `EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the reconciler when it can update. In this case, we won’t need to update, so we can crash. + +`HTMLBody` is declared like so: +```swift +struct HTMLBody: AnyHTML { + let tag: String = "body" + let innerHTML: String? = nil + let attributes: [String : String] = [:] + let listeners: [String : Listener] = [:] +} +``` + +## Mounting +Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for. + +```swift +public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { + // 1. + guard let html = mapAnyView( + host.view, + transform: { (html: AnyHTML) in html } + ) else { + // 2. + if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { + return parent + } + + return nil + } + + // 3. + let node = HTMLTarget(host.view, html) + parent.children.append(node) + return node +}} +``` + +1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be used with our `HTMLTarget`. +2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). +3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it. + +The other two functions required by the `Renderer` protocol can crash, as `TokamakStatic` doesn’t support state changes: + +```swift +public func update(target: HTMLTarget, with host: MountedHost) { + fatalError("Stateful apps cannot be created with TokamakStatic") +} + +public func unmount( + target: HTMLTarget, + from parent: HTMLTarget, + with host: MountedHost, + completion: @escaping () -> () +) { + fatalError("Stateful apps cannot be created with TokamakStatic") +} +``` + +If you are creating a `Renderer` that supports state changes, here’s a quick synopsis: + +* `func update` - Mutate the `target` to match the `host`. +* `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been removed. + +Now that we can mount, let’s give it a try: + +```swift +struct ContentView : View { + var body: some View { + Text("Hello, world!") + } +} + +let renderer = StaticRenderer(ContentView()) +print(renderer.html) +``` + +This spits out: + +```html + + + Hello, world! + + +``` + +Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll bring Tokamak to. + +#tokamak diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md new file mode 100644 index 000000000..5fde0d6ff --- /dev/null +++ b/docs/Building a Renderer/6 Providing platform-specific primitives.md @@ -0,0 +1,54 @@ +# Providing platform-specific primitives +Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the `StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. + +This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our `Renderer` understands. For instance, `TokamakDOM` (and `TokamakStatic` by extension) use the `HTML` view. Let’s look at a simpler version of this view: + +```swift +protocol AnyHTML { + let tag: String + let attributes: [String:String] + let innerHTML: String +} + +struct HTML: View, AnyHTML { + let tag: String + let attributes: [String:String] + let innerHTML: String + var body: Never { + neverBody("HTML") + } +} +``` + +Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without worrying about the `associatedtypes` involved with `View`. + +## `ViewDeferredToRenderer` + +Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`: + +```swift +extension Text: ViewDeferredToRenderer { + var deferredBody: AnyView { + AnyView(HTML("span", [:], _TextProxy(self).rawText)) + } +} +``` + +If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`: + +```swift +// 1. +guard let html = mapAnyView( + host.view, + transform: { (html: AnyHTML) in html } +) else { ... } +``` + +Then we were able to access the properties of the HTML. + +## Proxies +Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to access the storage of the `Text` view, we were required to use a `_TextProxy`. + +Proxies contain all of the properties of the primitive necessary to build your platform-specific implementation. + +#tokamak From 01e7df05172cf2715a3ee1abffadea588ff4a733 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Wed, 22 Jul 2020 16:46:49 -0400 Subject: [PATCH 03/10] Remove Bear tags --- docs/Building a Renderer/1 Renderers in Tokamak.md | 2 -- docs/Building a Renderer/2 Understanding Renderers.md | 2 -- docs/Building a Renderer/3 TokamakStatic Setup.md | 2 -- docs/Building a Renderer/4 Building the Target.md | 2 -- docs/Building a Renderer/5 Building the Renderer.md | 2 -- .../6 Providing platform-specific primitives.md | 2 -- 6 files changed, 12 deletions(-) diff --git a/docs/Building a Renderer/1 Renderers in Tokamak.md b/docs/Building a Renderer/1 Renderers in Tokamak.md index 73e8512fd..56f0cf668 100644 --- a/docs/Building a Renderer/1 Renderers in Tokamak.md +++ b/docs/Building a Renderer/1 Renderers in Tokamak.md @@ -11,5 +11,3 @@ Before we create the `Renderer`, we need to understand the requirements of our p `TokamakDOM` provides HTML representations of many `Views`, so we can utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can understand, and when you are required to do so. And that’s it! In the next part we’ll go more in depth on `Renderers`. - -#tokamak diff --git a/docs/Building a Renderer/2 Understanding Renderers.md b/docs/Building a Renderer/2 Understanding Renderers.md index e62767e88..64bc3b08f 100644 --- a/docs/Building a Renderer/2 Understanding Renderers.md +++ b/docs/Building a Renderer/2 Understanding Renderers.md @@ -8,5 +8,3 @@ So, what goes into a `Renderer`? 5. `func unmount` - This function is called when an existing target instance should be unmounted: removed from the parent and most likely destroyed. That’s it! Let’s get our project setup. - -#tokamak diff --git a/docs/Building a Renderer/3 TokamakStatic Setup.md b/docs/Building a Renderer/3 TokamakStatic Setup.md index 678d2ee27..496e26e03 100644 --- a/docs/Building a Renderer/3 TokamakStatic Setup.md +++ b/docs/Building a Renderer/3 TokamakStatic Setup.md @@ -75,5 +75,3 @@ extension Text { ``` We’ve omitted any stateful `Views`, as well as property wrappers used to modify state. - -#tokamak diff --git a/docs/Building a Renderer/4 Building the Target.md b/docs/Building a Renderer/4 Building the Target.md index fb0f442e6..8b8ab5912 100644 --- a/docs/Building a Renderer/4 Building the Target.md +++ b/docs/Building a Renderer/4 Building the Target.md @@ -40,5 +40,3 @@ extension HTMLTarget { } } ``` - -#tokamak diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md index c32db1b17..361c57aaa 100644 --- a/docs/Building a Renderer/5 Building the Renderer.md +++ b/docs/Building a Renderer/5 Building the Renderer.md @@ -120,5 +120,3 @@ This spits out: ``` Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll bring Tokamak to. - -#tokamak diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md index 5fde0d6ff..4ba02cbfa 100644 --- a/docs/Building a Renderer/6 Providing platform-specific primitives.md +++ b/docs/Building a Renderer/6 Providing platform-specific primitives.md @@ -50,5 +50,3 @@ Then we were able to access the properties of the HTML. Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to access the storage of the `Text` view, we were required to use a `_TextProxy`. Proxies contain all of the properties of the primitive necessary to build your platform-specific implementation. - -#tokamak From c940c29b86e1809dc5ca4e5d3f08cbf2633b1668 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Fri, 31 Jul 2020 10:45:35 -0400 Subject: [PATCH 04/10] Make TokamakDOM depend on TokamakStaticHTML --- Package.swift | 21 ++++--- Sources/TokamakDOM/App.swift | 1 + Sources/TokamakDOM/DOMNode.swift | 22 +++++++ Sources/TokamakDOM/DOMRenderer.swift | 13 +++-- Sources/TokamakDOM/Styles/ToggleStyle.swift | 3 +- Sources/TokamakDOM/Views/Buttons/Button.swift | 9 ++- .../Views/Containers/DisclosureGroup.swift | 15 ++--- Sources/TokamakDOM/Views/DynamicHTML.swift | 57 +++++++++++++++++++ .../{ => Navigation}/NavigationLink.swift | 2 +- .../TokamakDOM/Views/Selectors/Picker.swift | 3 +- .../TokamakDOM/Views/Text/SecureField.swift | 2 +- Sources/TokamakDOM/Views/Text/TextField.swift | 2 +- Sources/TokamakStaticDemo/ContentView.swift | 24 ++++++++ Sources/TokamakStaticDemo/main.swift | 20 ++++--- .../Core.swift | 0 .../Modifiers/Effects/RotationEffect.swift | 0 .../Modifiers/LayoutModifiers.swift | 0 .../Modifiers/ViewModifier.swift | 0 .../Modifiers/_ViewModifier_Content.swift | 0 .../Resources/TokamakStyles.swift | 2 +- .../Scenes/WindowGroup.swift | 0 .../Shapes/Path.swift | 0 .../Shapes/Shape.swift | 0 .../Shapes/_ShapeView.swift | 0 .../StaticRenderer.swift | 38 ++++++++++--- .../Tokens/Tokens.swift | 0 .../Views/Containers/List.swift | 0 .../Views/HTML.swift | 33 ++--------- .../Views/Layout/HStack.swift | 2 +- .../Views/Layout/LazyHGrid.swift | 8 +-- .../Views/Layout/LazyVGrid.swift | 8 +-- .../Views/Layout/ScrollView.swift | 2 +- .../Views/Layout/VStack.swift | 2 +- .../Views/Layout/ZStack.swift | 0 .../Views/NavigationView.swift | 1 - .../Views/Spacers/Divider.swift | 2 - .../Views/Spacers/Spacer.swift | 8 +-- .../Views/Text/Text.swift | 5 +- 38 files changed, 207 insertions(+), 98 deletions(-) create mode 100644 Sources/TokamakDOM/Views/DynamicHTML.swift rename Sources/TokamakDOM/Views/{ => Navigation}/NavigationLink.swift (97%) create mode 100644 Sources/TokamakStaticDemo/ContentView.swift rename Sources/{TokamakStatic => TokamakStaticHTML}/Core.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Modifiers/Effects/RotationEffect.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Modifiers/LayoutModifiers.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Modifiers/ViewModifier.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Modifiers/_ViewModifier_Content.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Resources/TokamakStyles.swift (97%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Scenes/WindowGroup.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Shapes/Path.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Shapes/Shape.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Shapes/_ShapeView.swift (100%) rename Sources/{TokamakStatic => TokamakStaticHTML}/StaticRenderer.swift (76%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Tokens/Tokens.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Containers/List.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/HTML.swift (63%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/HStack.swift (95%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/LazyHGrid.swift (91%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/LazyVGrid.swift (91%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/ScrollView.swift (97%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/VStack.swift (95%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Layout/ZStack.swift (100%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/NavigationView.swift (97%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Spacers/Divider.swift (94%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Spacers/Spacer.swift (92%) rename Sources/{TokamakDOM => TokamakStaticHTML}/Views/Text/Text.swift (97%) diff --git a/Package.swift b/Package.swift index 3d42dc116..a2e9ca67a 100644 --- a/Package.swift +++ b/Package.swift @@ -26,8 +26,8 @@ let package = Package( targets: ["TokamakShim"] ), .library( - name: "TokamakStatic", - targets: ["TokamakStatic"] + name: "TokamakStaticHTML", + targets: ["TokamakStaticHTML"] ), .executable( name: "TokamakStaticDemo", @@ -59,28 +59,27 @@ let package = Package( dependencies: ["CombineShim", "Runtime"] ), .target( - name: "TokamakDemo", - dependencies: ["JavaScriptKit", "TokamakShim"] + name: "TokamakStaticHTML", + dependencies: [ + "TokamakCore", + ] ), .target( name: "TokamakDOM", - dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore"] + dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore", "TokamakStaticHTML"] ), .target( name: "TokamakShim", dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))] ), .target( - name: "TokamakStatic", - dependencies: [ - "TokamakCore", - "TokamakDOM", - ] + name: "TokamakDemo", + dependencies: ["JavaScriptKit", "TokamakShim"] ), .target( name: "TokamakStaticDemo", dependencies: [ - "TokamakStatic", + "TokamakStaticHTML", ] ), .target( diff --git a/Sources/TokamakDOM/App.swift b/Sources/TokamakDOM/App.swift index c0e8af7b5..c7752a996 100644 --- a/Sources/TokamakDOM/App.swift +++ b/Sources/TokamakDOM/App.swift @@ -18,6 +18,7 @@ import CombineShim import JavaScriptKit import TokamakCore +import TokamakStaticHTML private enum ScenePhaseObserver { static var publisher = CurrentValueSubject(.active) diff --git a/Sources/TokamakDOM/DOMNode.swift b/Sources/TokamakDOM/DOMNode.swift index 46761b58e..dc94ad60d 100644 --- a/Sources/TokamakDOM/DOMNode.swift +++ b/Sources/TokamakDOM/DOMNode.swift @@ -14,6 +14,28 @@ import JavaScriptKit import TokamakCore +import TokamakStaticHTML + +extension AnyHTML { + func update(dom: DOMNode) { + // FIXME: is there a sensible way to diff attributes and listeners to avoid + // crossing the JavaScript bridge and touching DOM if not needed? + + // @carson-katri: For diffing, could you build a Set from the keys and values of the dictionary, + // then use the standard lib to get the difference? + + for (attribute, value) in attributes { + _ = dom.ref[dynamicMember: attribute] = .string(value) + } + + if let dynamicSelf = self as? AnyDynamicHTML { + dom.reinstall(dynamicSelf.listeners) + } + + guard let innerHTML = innerHTML else { return } + dom.ref.innerHTML = .string(innerHTML) + } +} public final class DOMNode: Target { let ref: JSObjectRef diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 6723e8859..afbb2ebe7 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -17,6 +17,7 @@ import JavaScriptKit import TokamakCore +import TokamakStaticHTML extension EnvironmentValues { /// Returns default settings for the DOM environment @@ -100,9 +101,9 @@ public final class DOMRenderer: Renderer { } public func mountTarget(to parent: DOMNode, with host: MountedHost) -> DOMNode? { - guard let (outerHTML, listeners) = mapAnyView( + guard let anyHTML = mapAnyView( host.view, - transform: { (html: AnyHTML) in (html.outerHTML, html.listeners) } + transform: { (html: AnyHTML) in html } ) else { // handle cases like `TupleView` if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { @@ -112,7 +113,7 @@ public final class DOMRenderer: Renderer { return nil } - _ = parent.ref.insertAdjacentHTML!("beforeend", JSValue(stringLiteral: outerHTML)) + _ = parent.ref.insertAdjacentHTML!("beforeend", JSValue(stringLiteral: anyHTML.outerHTML)) guard let children = parent.ref.childNodes.object, @@ -129,7 +130,11 @@ public final class DOMRenderer: Renderer { lastChild.style.object!.height = "100%" } - return DOMNode(host.view, lastChild, listeners) + if let dynamicHTML = anyHTML as? AnyDynamicHTML { + return DOMNode(host.view, lastChild, dynamicHTML.listeners) + } else { + return DOMNode(host.view, lastChild, [:]) + } } public func update(target: DOMNode, with host: MountedHost) { diff --git a/Sources/TokamakDOM/Styles/ToggleStyle.swift b/Sources/TokamakDOM/Styles/ToggleStyle.swift index 945683ab3..93bb7e313 100644 --- a/Sources/TokamakDOM/Styles/ToggleStyle.swift +++ b/Sources/TokamakDOM/Styles/ToggleStyle.swift @@ -16,6 +16,7 @@ // import TokamakCore +import TokamakStaticHTML public struct DefaultToggleStyle: ToggleStyle { public func makeBody(configuration: Configuration) -> some View { @@ -32,7 +33,7 @@ public struct CheckboxToggleStyle: ToggleStyle { attrs["checked"] = "checked" } return HTML("label") { - HTML("input", attrs, listeners: [ + DynamicHTML("input", attrs, listeners: [ "change": { event in let checked = event.target.object?.checked.boolean ?? false configuration.isOn = checked diff --git a/Sources/TokamakDOM/Views/Buttons/Button.swift b/Sources/TokamakDOM/Views/Buttons/Button.swift index 49bea4c81..a6859269a 100644 --- a/Sources/TokamakDOM/Views/Buttons/Button.swift +++ b/Sources/TokamakDOM/Views/Buttons/Button.swift @@ -19,8 +19,11 @@ import TokamakCore extension Button: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { - AnyView(HTML("button", listeners: ["click": { _ in _ButtonProxy(self).action() }]) { - _ButtonProxy(self).label.subject - }) + AnyView(DynamicHTML("button", + listeners: ["click": { _ in + _ButtonProxy(self).action() + }]) { + _ButtonProxy(self).label.subject + }) } } diff --git a/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift b/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift index 6b09857f2..c0dbe8e51 100644 --- a/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift +++ b/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift @@ -16,16 +16,17 @@ // import TokamakCore +import TokamakStaticHTML extension DisclosureGroup: ViewDeferredToRenderer { var chevron: some View { - HTML("div", - ["class": "_tokamak-disclosuregroup-chevron-container"], - listeners: [ - "click": { _ in - _DisclosureGroupProxy(self).toggleIsExpanded() - }, - ]) { + DynamicHTML("div", + ["class": "_tokamak-disclosuregroup-chevron-container"], + listeners: [ + "click": { _ in + _DisclosureGroupProxy(self).toggleIsExpanded() + }, + ]) { HTML("div", ["class": "_tokamak-disclosuregroup-chevron"]) .rotationEffect(_DisclosureGroupProxy(self).isExpanded ? .degrees(90) : diff --git a/Sources/TokamakDOM/Views/DynamicHTML.swift b/Sources/TokamakDOM/Views/DynamicHTML.swift new file mode 100644 index 000000000..2a8b63fd3 --- /dev/null +++ b/Sources/TokamakDOM/Views/DynamicHTML.swift @@ -0,0 +1,57 @@ +// +// File.swift +// +// +// Created by Carson Katri on 7/31/20. +// + +import JavaScriptKit +import TokamakCore +import TokamakStaticHTML + +public typealias Listener = (JSObjectRef) -> () + +protocol AnyDynamicHTML: AnyHTML { + var listeners: [String: Listener] { get } +} + +public struct DynamicHTML: View, AnyDynamicHTML where Content: View { + public let tag: String + public let attributes: [String: String] + public let listeners: [String: Listener] + let content: Content + + public init( + _ tag: String, + _ attributes: [String: String] = [:], + listeners: [String: Listener] = [:], + @ViewBuilder content: () -> Content + ) { + self.tag = tag + self.attributes = attributes + self.listeners = listeners + self.content = content() + } + + public var innerHTML: String? { nil } + + public var body: Never { + neverBody("HTML") + } +} + +extension DynamicHTML where Content == EmptyView { + public init( + _ tag: String, + _ attributes: [String: String] = [:], + listeners: [String: Listener] = [:] + ) { + self = DynamicHTML(tag, attributes, listeners: listeners) { EmptyView() } + } +} + +extension DynamicHTML: ParentView { + public var children: [AnyView] { + [AnyView(content)] + } +} diff --git a/Sources/TokamakDOM/Views/NavigationLink.swift b/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift similarity index 97% rename from Sources/TokamakDOM/Views/NavigationLink.swift rename to Sources/TokamakDOM/Views/Navigation/NavigationLink.swift index 85a3be3ad..da179da49 100644 --- a/Sources/TokamakDOM/Views/NavigationLink.swift +++ b/Sources/TokamakDOM/Views/Navigation/NavigationLink.swift @@ -18,7 +18,7 @@ extension NavigationLink: ViewDeferredToRenderer { public var deferredBody: AnyView { let proxy = _NavigationLinkProxy(self) return AnyView( - HTML("a", [ + DynamicHTML("a", [ "href": "javascript:void%200", ], listeners: [ // FIXME: Focus destination or something so assistive diff --git a/Sources/TokamakDOM/Views/Selectors/Picker.swift b/Sources/TokamakDOM/Views/Selectors/Picker.swift index 5fa076e5d..7289a7d8a 100644 --- a/Sources/TokamakDOM/Views/Selectors/Picker.swift +++ b/Sources/TokamakDOM/Views/Selectors/Picker.swift @@ -14,13 +14,14 @@ import JavaScriptKit import TokamakCore +import TokamakStaticHTML extension _PickerContainer: ViewDeferredToRenderer { public var deferredBody: AnyView { AnyView(HTML("label") { label Text(" ") - HTML("select", listeners: ["change": { + DynamicHTML("select", listeners: ["change": { guard let valueString = $0.target.object!.value.string, let value = Int(valueString) as? SelectionValue diff --git a/Sources/TokamakDOM/Views/Text/SecureField.swift b/Sources/TokamakDOM/Views/Text/SecureField.swift index 18018e694..176f517e8 100644 --- a/Sources/TokamakDOM/Views/Text/SecureField.swift +++ b/Sources/TokamakDOM/Views/Text/SecureField.swift @@ -20,7 +20,7 @@ import TokamakCore extension SecureField: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { let proxy = _SecureFieldProxy(self) - return AnyView(HTML("input", [ + return AnyView(DynamicHTML("input", [ "type": "password", "value": proxy.textBinding.wrappedValue, "placeholder": proxy.label.rawText, diff --git a/Sources/TokamakDOM/Views/Text/TextField.swift b/Sources/TokamakDOM/Views/Text/TextField.swift index d0c4dbcb4..71d820fcc 100644 --- a/Sources/TokamakDOM/Views/Text/TextField.swift +++ b/Sources/TokamakDOM/Views/Text/TextField.swift @@ -32,7 +32,7 @@ extension TextField: ViewDeferredToRenderer where Label == Text { public var deferredBody: AnyView { let proxy = _TextFieldProxy(self) - return AnyView(HTML("input", [ + return AnyView(DynamicHTML("input", [ "type": proxy.textFieldStyle is RoundedBorderTextFieldStyle ? "search" : "text", "value": proxy.textBinding.wrappedValue, "placeholder": proxy.label.rawText, diff --git a/Sources/TokamakStaticDemo/ContentView.swift b/Sources/TokamakStaticDemo/ContentView.swift new file mode 100644 index 000000000..597e68032 --- /dev/null +++ b/Sources/TokamakStaticDemo/ContentView.swift @@ -0,0 +1,24 @@ +// Copyright 2020 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 7/31/20. +// + +import TokamakStaticHTML + +struct ContentView: View { + var body: some View { + Text("Hello, world!") + } +} diff --git a/Sources/TokamakStaticDemo/main.swift b/Sources/TokamakStaticDemo/main.swift index c37373acb..fac623b4f 100644 --- a/Sources/TokamakStaticDemo/main.swift +++ b/Sources/TokamakStaticDemo/main.swift @@ -1,17 +1,21 @@ +// Copyright 2020 Tokamak contributors // -// File.swift +// 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 7/20/20. // -import TokamakStatic - -struct ContentView: View { - var body: some View { - Text("Hello, world!") - } -} +import TokamakStaticHTML let renderer = StaticRenderer(ContentView()) print(renderer.html) diff --git a/Sources/TokamakStatic/Core.swift b/Sources/TokamakStaticHTML/Core.swift similarity index 100% rename from Sources/TokamakStatic/Core.swift rename to Sources/TokamakStaticHTML/Core.swift diff --git a/Sources/TokamakDOM/Modifiers/Effects/RotationEffect.swift b/Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift similarity index 100% rename from Sources/TokamakDOM/Modifiers/Effects/RotationEffect.swift rename to Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift diff --git a/Sources/TokamakDOM/Modifiers/LayoutModifiers.swift b/Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift similarity index 100% rename from Sources/TokamakDOM/Modifiers/LayoutModifiers.swift rename to Sources/TokamakStaticHTML/Modifiers/LayoutModifiers.swift diff --git a/Sources/TokamakDOM/Modifiers/ViewModifier.swift b/Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift similarity index 100% rename from Sources/TokamakDOM/Modifiers/ViewModifier.swift rename to Sources/TokamakStaticHTML/Modifiers/ViewModifier.swift diff --git a/Sources/TokamakDOM/Modifiers/_ViewModifier_Content.swift b/Sources/TokamakStaticHTML/Modifiers/_ViewModifier_Content.swift similarity index 100% rename from Sources/TokamakDOM/Modifiers/_ViewModifier_Content.swift rename to Sources/TokamakStaticHTML/Modifiers/_ViewModifier_Content.swift diff --git a/Sources/TokamakDOM/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift similarity index 97% rename from Sources/TokamakDOM/Resources/TokamakStyles.swift rename to Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index 2dc57001e..26f40e610 100644 --- a/Sources/TokamakDOM/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -55,7 +55,7 @@ public let tokamakStyles = """ } """ -let rootNodeStyles = """ +public let rootNodeStyles = """ display: flex; width: 100%; height: 100%; diff --git a/Sources/TokamakDOM/Scenes/WindowGroup.swift b/Sources/TokamakStaticHTML/Scenes/WindowGroup.swift similarity index 100% rename from Sources/TokamakDOM/Scenes/WindowGroup.swift rename to Sources/TokamakStaticHTML/Scenes/WindowGroup.swift diff --git a/Sources/TokamakDOM/Shapes/Path.swift b/Sources/TokamakStaticHTML/Shapes/Path.swift similarity index 100% rename from Sources/TokamakDOM/Shapes/Path.swift rename to Sources/TokamakStaticHTML/Shapes/Path.swift diff --git a/Sources/TokamakDOM/Shapes/Shape.swift b/Sources/TokamakStaticHTML/Shapes/Shape.swift similarity index 100% rename from Sources/TokamakDOM/Shapes/Shape.swift rename to Sources/TokamakStaticHTML/Shapes/Shape.swift diff --git a/Sources/TokamakDOM/Shapes/_ShapeView.swift b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift similarity index 100% rename from Sources/TokamakDOM/Shapes/_ShapeView.swift rename to Sources/TokamakStaticHTML/Shapes/_ShapeView.swift diff --git a/Sources/TokamakStatic/StaticRenderer.swift b/Sources/TokamakStaticHTML/StaticRenderer.swift similarity index 76% rename from Sources/TokamakStatic/StaticRenderer.swift rename to Sources/TokamakStaticHTML/StaticRenderer.swift index fbb572e28..685f8ec5b 100644 --- a/Sources/TokamakStatic/StaticRenderer.swift +++ b/Sources/TokamakStaticHTML/StaticRenderer.swift @@ -16,7 +16,6 @@ // import TokamakCore -import TokamakDOM public final class HTMLTarget: Target { var html: AnyHTML @@ -27,6 +26,12 @@ public final class HTMLTarget: Target { self.html = html super.init(view) } + + init(_ app: A, + _ html: AnyHTML) { + self.html = html + super.init(app) + } } extension HTMLTarget { @@ -44,8 +49,9 @@ extension HTMLTarget { struct HTMLBody: AnyHTML { let tag: String = "body" let innerHTML: String? = nil - let attributes: [String: String] = [:] - let listeners: [String: Listener] = [:] + let attributes: [String: String] = [ + "style": "margin: 0;" + rootNodeStyles, + ] } public final class StaticRenderer: Renderer { @@ -66,16 +72,32 @@ public final class StaticRenderer: Renderer { """ } - public init(_ view: V) { + public init(_ view: V, _ rootEnvironment: EnvironmentValues? = nil) { rootTarget = HTMLTarget(view, HTMLBody()) + reconciler = StackReconciler( view: view, target: rootTarget, + environment: EnvironmentValues(), renderer: self, - environment: EnvironmentValues() - ) { _ in - fatalError("Stateful apps cannot be created with TokamakStatic") - } + scheduler: { _ in + fatalError("Stateful apps cannot be created with TokamakStatic") + } + ) + } + + public init(_ app: A, _ rootEnvironment: EnvironmentValues? = nil) { + rootTarget = HTMLTarget(app, HTMLBody()) + + reconciler = StackReconciler( + app: app, + target: rootTarget, + environment: EnvironmentValues(), + renderer: self, + scheduler: { _ in + fatalError("Stateful apps cannot be created with TokamakStatic") + } + ) } public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { diff --git a/Sources/TokamakDOM/Tokens/Tokens.swift b/Sources/TokamakStaticHTML/Tokens/Tokens.swift similarity index 100% rename from Sources/TokamakDOM/Tokens/Tokens.swift rename to Sources/TokamakStaticHTML/Tokens/Tokens.swift diff --git a/Sources/TokamakDOM/Views/Containers/List.swift b/Sources/TokamakStaticHTML/Views/Containers/List.swift similarity index 100% rename from Sources/TokamakDOM/Views/Containers/List.swift rename to Sources/TokamakStaticHTML/Views/Containers/List.swift diff --git a/Sources/TokamakDOM/Views/HTML.swift b/Sources/TokamakStaticHTML/Views/HTML.swift similarity index 63% rename from Sources/TokamakDOM/Views/HTML.swift rename to Sources/TokamakStaticHTML/Views/HTML.swift index 79313593a..0cf013ac9 100644 --- a/Sources/TokamakDOM/Views/HTML.swift +++ b/Sources/TokamakStaticHTML/Views/HTML.swift @@ -15,16 +15,12 @@ // Created by Max Desiatov on 11/04/2020. // -import JavaScriptKit import TokamakCore -public typealias Listener = (JSObjectRef) -> () - public protocol AnyHTML { var innerHTML: String? { get } var tag: String { get } var attributes: [String: String] { get } - var listeners: [String: Listener] { get } } extension AnyHTML { @@ -36,40 +32,20 @@ extension AnyHTML { """ } - - func update(dom: DOMNode) { - // FIXME: is there a sensible way to diff attributes and listeners to avoid - // crossing the JavaScript bridge and touching DOM if not needed? - - // @carson-katri: For diffing, could you build a Set from the keys and values of the dictionary, - // then use the standard lib to get the difference? - - for (attribute, value) in attributes { - _ = dom.ref[dynamicMember: attribute] = .string(value) - } - - dom.reinstall(listeners) - - guard let innerHTML = innerHTML else { return } - dom.ref.innerHTML = .string(innerHTML) - } } public struct HTML: View, AnyHTML where Content: View { public let tag: String public let attributes: [String: String] - public let listeners: [String: Listener] let content: Content public init( _ tag: String, _ attributes: [String: String] = [:], - listeners: [String: Listener] = [:], @ViewBuilder content: () -> Content ) { self.tag = tag self.attributes = attributes - self.listeners = listeners self.content = content() } @@ -83,10 +59,9 @@ public struct HTML: View, AnyHTML where Content: View { extension HTML where Content == EmptyView { public init( _ tag: String, - _ attributes: [String: String] = [:], - listeners: [String: Listener] = [:] + _ attributes: [String: String] = [:] ) { - self = HTML(tag, attributes, listeners: listeners) { EmptyView() } + self = HTML(tag, attributes) { EmptyView() } } } @@ -96,12 +71,12 @@ extension HTML: ParentView { } } -protocol StylesConvertible { +public protocol StylesConvertible { var styles: [String: String] { get } } extension Dictionary { - var inlineStyles: String { + public var inlineStyles: String { map { "\($0.0): \($0.1);" } .joined(separator: " ") } diff --git a/Sources/TokamakDOM/Views/Layout/HStack.swift b/Sources/TokamakStaticHTML/Views/Layout/HStack.swift similarity index 95% rename from Sources/TokamakDOM/Views/Layout/HStack.swift rename to Sources/TokamakStaticHTML/Views/Layout/HStack.swift index 0139fb530..b302b68e9 100644 --- a/Sources/TokamakDOM/Views/Layout/HStack.swift +++ b/Sources/TokamakStaticHTML/Views/Layout/HStack.swift @@ -28,7 +28,7 @@ extension VerticalAlignment { } extension HStack: ViewDeferredToRenderer, SpacerContainer { - var axis: SpacerContainerAxis { .horizontal } + public var axis: SpacerContainerAxis { .horizontal } public var deferredBody: AnyView { AnyView(HTML("div", [ diff --git a/Sources/TokamakDOM/Views/Layout/LazyHGrid.swift b/Sources/TokamakStaticHTML/Views/Layout/LazyHGrid.swift similarity index 91% rename from Sources/TokamakDOM/Views/Layout/LazyHGrid.swift rename to Sources/TokamakStaticHTML/Views/Layout/LazyHGrid.swift index f7f9177e8..c9548a44b 100644 --- a/Sources/TokamakDOM/Views/Layout/LazyHGrid.swift +++ b/Sources/TokamakStaticHTML/Views/Layout/LazyHGrid.swift @@ -18,9 +18,9 @@ import TokamakCore extension LazyHGrid: SpacerContainer { - var axis: SpacerContainerAxis { .horizontal } - var hasSpacer: Bool { false } - var fillCrossAxis: Bool { + public var axis: SpacerContainerAxis { .horizontal } + public var hasSpacer: Bool { false } + public var fillCrossAxis: Bool { _LazyHGridProxy(self).rows.contains { if case .adaptive(minimum: _, maximum: _) = $0.size { return true @@ -32,7 +32,7 @@ extension LazyHGrid: SpacerContainer { } extension LazyHGrid: ViewDeferredToRenderer { - var lastRow: GridItem? { + public var lastRow: GridItem? { _LazyHGridProxy(self).rows.last } diff --git a/Sources/TokamakDOM/Views/Layout/LazyVGrid.swift b/Sources/TokamakStaticHTML/Views/Layout/LazyVGrid.swift similarity index 91% rename from Sources/TokamakDOM/Views/Layout/LazyVGrid.swift rename to Sources/TokamakStaticHTML/Views/Layout/LazyVGrid.swift index a18279e13..f9e94e342 100644 --- a/Sources/TokamakDOM/Views/Layout/LazyVGrid.swift +++ b/Sources/TokamakStaticHTML/Views/Layout/LazyVGrid.swift @@ -18,9 +18,9 @@ import TokamakCore extension LazyVGrid: SpacerContainer { - var axis: SpacerContainerAxis { .vertical } - var hasSpacer: Bool { false } - var fillCrossAxis: Bool { + public var axis: SpacerContainerAxis { .vertical } + public var hasSpacer: Bool { false } + public var fillCrossAxis: Bool { _LazyVGridProxy(self).columns.contains { if case .adaptive(minimum: _, maximum: _) = $0.size { return true @@ -32,7 +32,7 @@ extension LazyVGrid: SpacerContainer { } extension LazyVGrid: ViewDeferredToRenderer { - var lastColumn: GridItem? { + public var lastColumn: GridItem? { _LazyVGridProxy(self).columns.last } diff --git a/Sources/TokamakDOM/Views/Layout/ScrollView.swift b/Sources/TokamakStaticHTML/Views/Layout/ScrollView.swift similarity index 97% rename from Sources/TokamakDOM/Views/Layout/ScrollView.swift rename to Sources/TokamakStaticHTML/Views/Layout/ScrollView.swift index b9d873e77..ae8e884f9 100644 --- a/Sources/TokamakDOM/Views/Layout/ScrollView.swift +++ b/Sources/TokamakStaticHTML/Views/Layout/ScrollView.swift @@ -18,7 +18,7 @@ import TokamakCore extension ScrollView: ViewDeferredToRenderer, SpacerContainer { - var axis: SpacerContainerAxis { + public var axis: SpacerContainerAxis { if axes.contains(.horizontal) { return .horizontal } else { diff --git a/Sources/TokamakDOM/Views/Layout/VStack.swift b/Sources/TokamakStaticHTML/Views/Layout/VStack.swift similarity index 95% rename from Sources/TokamakDOM/Views/Layout/VStack.swift rename to Sources/TokamakStaticHTML/Views/Layout/VStack.swift index 36371d7bb..0483f6ff8 100644 --- a/Sources/TokamakDOM/Views/Layout/VStack.swift +++ b/Sources/TokamakStaticHTML/Views/Layout/VStack.swift @@ -28,7 +28,7 @@ extension HorizontalAlignment { } extension VStack: ViewDeferredToRenderer, SpacerContainer { - var axis: SpacerContainerAxis { .vertical } + public var axis: SpacerContainerAxis { .vertical } public var deferredBody: AnyView { AnyView(HTML("div", [ diff --git a/Sources/TokamakDOM/Views/Layout/ZStack.swift b/Sources/TokamakStaticHTML/Views/Layout/ZStack.swift similarity index 100% rename from Sources/TokamakDOM/Views/Layout/ZStack.swift rename to Sources/TokamakStaticHTML/Views/Layout/ZStack.swift diff --git a/Sources/TokamakDOM/Views/NavigationView.swift b/Sources/TokamakStaticHTML/Views/NavigationView.swift similarity index 97% rename from Sources/TokamakDOM/Views/NavigationView.swift rename to Sources/TokamakStaticHTML/Views/NavigationView.swift index 297548cfd..e3cc63f96 100644 --- a/Sources/TokamakDOM/Views/NavigationView.swift +++ b/Sources/TokamakStaticHTML/Views/NavigationView.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import JavaScriptKit import TokamakCore extension NavigationView: ViewDeferredToRenderer { diff --git a/Sources/TokamakDOM/Views/Spacers/Divider.swift b/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift similarity index 94% rename from Sources/TokamakDOM/Views/Spacers/Divider.swift rename to Sources/TokamakStaticHTML/Views/Spacers/Divider.swift index a04d5846f..57358bdda 100644 --- a/Sources/TokamakDOM/Views/Spacers/Divider.swift +++ b/Sources/TokamakStaticHTML/Views/Spacers/Divider.swift @@ -28,6 +28,4 @@ extension Divider: AnyHTML { """, ] } - - public var listeners: [String: Listener] { [:] } } diff --git a/Sources/TokamakDOM/Views/Spacers/Spacer.swift b/Sources/TokamakStaticHTML/Views/Spacers/Spacer.swift similarity index 92% rename from Sources/TokamakDOM/Views/Spacers/Spacer.swift rename to Sources/TokamakStaticHTML/Views/Spacers/Spacer.swift index 0ba11a88a..7fef197a5 100644 --- a/Sources/TokamakDOM/Views/Spacers/Spacer.swift +++ b/Sources/TokamakStaticHTML/Views/Spacers/Spacer.swift @@ -14,18 +14,18 @@ import TokamakCore -enum SpacerContainerAxis { +public enum SpacerContainerAxis { case horizontal, vertical } -protocol SpacerContainer { +public protocol SpacerContainer { var hasSpacer: Bool { get } var axis: SpacerContainerAxis { get } var fillCrossAxis: Bool { get } } extension SpacerContainer where Self: ParentView { - var hasSpacer: Bool { + public var hasSpacer: Bool { children .compactMap { mapAnyView($0) { (v: Spacer) in @@ -45,7 +45,7 @@ extension SpacerContainer where Self: ParentView { // Does a child SpacerContainer along the opposite axis have a spacer? // (e.g., an HStack with a child VStack which contains a spacer) // If so, we need to fill the cross-axis so the child can show the correct layout. - var fillCrossAxis: Bool { + public var fillCrossAxis: Bool { children .compactMap { mapAnyView($0) { (v: SpacerContainer) in v } diff --git a/Sources/TokamakDOM/Views/Text/Text.swift b/Sources/TokamakStaticHTML/Views/Text/Text.swift similarity index 97% rename from Sources/TokamakDOM/Views/Text/Text.swift rename to Sources/TokamakStaticHTML/Views/Text/Text.swift index 13e2af94c..cc7d7f140 100644 --- a/Sources/TokamakDOM/Views/Text/Text.swift +++ b/Sources/TokamakStaticHTML/Views/Text/Text.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import JavaScriptKit import TokamakCore extension Font.Design: CustomStringConvertible { @@ -78,7 +77,7 @@ extension Font.Leading: CustomStringConvertible { } extension Font: StylesConvertible { - var styles: [String: String] { + public var styles: [String: String] { [ "font-family": _name == _FontNames.system.rawValue ? _design.description : _name, "font-weight": "\(_bold ? Font.Weight.bold.value : _weight.value)", @@ -159,6 +158,4 @@ extension Text: AnyHTML { """, ] } - - public var listeners: [String: Listener] { [:] } } From e7c90e43fa8471aed3da67d5b4328d89dc139247 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Fri, 31 Jul 2020 22:58:32 -0400 Subject: [PATCH 05/10] Support App/Scene --- Sources/TokamakDemo/AppStorageDemo.swift | 6 +--- Sources/TokamakDemo/ToggleDemo.swift | 7 +--- Sources/TokamakStaticDemo/main.swift | 11 +++++-- Sources/TokamakStaticHTML/App.swift | 33 +++++++++++++++++++ Sources/TokamakStaticHTML/Core.swift | 9 +++++ .../TokamakStaticHTML/StaticRenderer.swift | 2 ++ 6 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 Sources/TokamakStaticHTML/App.swift diff --git a/Sources/TokamakDemo/AppStorageDemo.swift b/Sources/TokamakDemo/AppStorageDemo.swift index cb37111c4..fca69cc8b 100644 --- a/Sources/TokamakDemo/AppStorageDemo.swift +++ b/Sources/TokamakDemo/AppStorageDemo.swift @@ -15,11 +15,7 @@ // Created by Carson Katri on 7/17/20. // -#if canImport(SwiftUI) -import SwiftUI -#else -import TokamakDOM -#endif +import TokamakShim @available(OSX 11.0, iOS 14.0, *) struct AppStorageButtons: View { diff --git a/Sources/TokamakDemo/ToggleDemo.swift b/Sources/TokamakDemo/ToggleDemo.swift index 2c8a969ad..e7618fde4 100644 --- a/Sources/TokamakDemo/ToggleDemo.swift +++ b/Sources/TokamakDemo/ToggleDemo.swift @@ -12,12 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if canImport(SwiftUI) -import SwiftUI -#else -import TokamakCore -import TokamakDOM -#endif +import TokamakShim public struct ToggleDemo: View { @State var checked = false diff --git a/Sources/TokamakStaticDemo/main.swift b/Sources/TokamakStaticDemo/main.swift index fac623b4f..e521780c5 100644 --- a/Sources/TokamakStaticDemo/main.swift +++ b/Sources/TokamakStaticDemo/main.swift @@ -17,5 +17,12 @@ import TokamakStaticHTML -let renderer = StaticRenderer(ContentView()) -print(renderer.html) +struct TestApp: App { + var body: some Scene { + WindowGroup("TokamakStaticHTML Demo") { + ContentView() + } + } +} + +print(StaticRenderer(TestApp()).html) diff --git a/Sources/TokamakStaticHTML/App.swift b/Sources/TokamakStaticHTML/App.swift new file mode 100644 index 000000000..d9d4752c7 --- /dev/null +++ b/Sources/TokamakStaticHTML/App.swift @@ -0,0 +1,33 @@ +// Copyright 2020 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 7/31/20. +// + +import CombineShim +import TokamakCore + +extension App { + public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { + fatalError("TokamakStaticHTML does not support default `App._launch`") + } + + public static func _setTitle(_ title: String) { + StaticRenderer.title = title + } + + public var _phasePublisher: CurrentValueSubject { + CurrentValueSubject(.active) + } +} diff --git a/Sources/TokamakStaticHTML/Core.swift b/Sources/TokamakStaticHTML/Core.swift index e0482f770..35963d91f 100644 --- a/Sources/TokamakStaticHTML/Core.swift +++ b/Sources/TokamakStaticHTML/Core.swift @@ -76,6 +76,15 @@ public typealias View = TokamakCore.View public typealias AnyView = TokamakCore.AnyView public typealias EmptyView = TokamakCore.EmptyView +// MARK: App & Scene + +public typealias App = TokamakCore.App +public typealias Scene = TokamakCore.Scene +public typealias WindowGroup = TokamakCore.WindowGroup +public typealias ScenePhase = TokamakCore.ScenePhase +public typealias AppStorage = TokamakCore.AppStorage +public typealias SceneStorage = TokamakCore.SceneStorage + // MARK: Misc // FIXME: I would put this inside TokamakCore, but for diff --git a/Sources/TokamakStaticHTML/StaticRenderer.swift b/Sources/TokamakStaticHTML/StaticRenderer.swift index 685f8ec5b..c204f71cb 100644 --- a/Sources/TokamakStaticHTML/StaticRenderer.swift +++ b/Sources/TokamakStaticHTML/StaticRenderer.swift @@ -59,10 +59,12 @@ public final class StaticRenderer: Renderer { var rootTarget: HTMLTarget + static var title: String = "" public var html: String { """ + \(Self.title) From 9c4162e9272c5db083787165518a9e0eb41c6cb8 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sat, 1 Aug 2020 08:13:21 -0400 Subject: [PATCH 06/10] Fix formatting in article 6 --- .../6 Providing platform-specific primitives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md index 4ba02cbfa..3fa90cdc9 100644 --- a/docs/Building a Renderer/6 Providing platform-specific primitives.md +++ b/docs/Building a Renderer/6 Providing platform-specific primitives.md @@ -29,7 +29,7 @@ Now we can use `HTML` to override the body of the primitive `Views` provided by ```swift extension Text: ViewDeferredToRenderer { var deferredBody: AnyView { - AnyView(HTML("span", [:], _TextProxy(self).rawText)) + AnyView(HTML("span", [:], _TextProxy(self).rawText)) } } ``` From e25814bef9ec74522dca3a1a8b85e0841e20478f Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sat, 1 Aug 2020 15:12:48 -0400 Subject: [PATCH 07/10] Add license to DynamicHTML.swift --- Sources/TokamakDOM/Views/DynamicHTML.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/TokamakDOM/Views/DynamicHTML.swift b/Sources/TokamakDOM/Views/DynamicHTML.swift index 2a8b63fd3..4d8ac37c8 100644 --- a/Sources/TokamakDOM/Views/DynamicHTML.swift +++ b/Sources/TokamakDOM/Views/DynamicHTML.swift @@ -1,6 +1,16 @@ +// Copyright 2020 Tokamak contributors // -// File.swift +// 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 7/31/20. // From e06f49aeffb131e4357298044f94aea22490a53d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 1 Aug 2020 20:20:48 +0100 Subject: [PATCH 08/10] Make names in documentation consistent --- .../Views/Containers/DisclosureGroup.swift | 16 +++++++++------- .../1 Renderers in Tokamak.md | 7 ++++--- .../Building a Renderer/3 TokamakStatic Setup.md | 5 +++-- .../Building a Renderer/4 Building the Target.md | 7 ++++--- .../5 Building the Renderer.md | 2 +- .../6 Providing platform-specific primitives.md | 8 +++++--- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift b/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift index c0dbe8e51..18f844467 100644 --- a/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift +++ b/Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift @@ -20,13 +20,15 @@ import TokamakStaticHTML extension DisclosureGroup: ViewDeferredToRenderer { var chevron: some View { - DynamicHTML("div", - ["class": "_tokamak-disclosuregroup-chevron-container"], - listeners: [ - "click": { _ in - _DisclosureGroupProxy(self).toggleIsExpanded() - }, - ]) { + DynamicHTML( + "div", + ["class": "_tokamak-disclosuregroup-chevron-container"], + listeners: [ + "click": { _ in + _DisclosureGroupProxy(self).toggleIsExpanded() + }, + ] + ) { HTML("div", ["class": "_tokamak-disclosuregroup-chevron"]) .rotationEffect(_DisclosureGroupProxy(self).isExpanded ? .degrees(90) : diff --git a/docs/Building a Renderer/1 Renderers in Tokamak.md b/docs/Building a Renderer/1 Renderers in Tokamak.md index 56f0cf668..88fc2a663 100644 --- a/docs/Building a Renderer/1 Renderers in Tokamak.md +++ b/docs/Building a Renderer/1 Renderers in Tokamak.md @@ -1,13 +1,14 @@ # `Renderers` in Tokamak + Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use to construct a representation of `Views` that your platform understands. -To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStatic` (which you can find in the `Tokamak` repository). +To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which you can find in the `Tokamak` repository). Before we create the `Renderer`, we need to understand the requirements of our platform: 1. Stateful apps cannot be created - This simplifies the scope of our project, as we only have to render once. However, if you are building a `Renderer` that supports state changes, the process is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a `View` has to be redrawn. + This simplifies the scope of our project, as we only have to render once. However, if you are building a `Renderer` that supports state changes, the process is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a `View` has to be redrawn. 2. HTML should be rendered - `TokamakDOM` provides HTML representations of many `Views`, so we can utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can understand, and when you are required to do so. + `TokamakDOM` provides HTML representations of many `Views`, so we can utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can understand, and when you are required to do so. And that’s it! In the next part we’ll go more in depth on `Renderers`. diff --git a/docs/Building a Renderer/3 TokamakStatic Setup.md b/docs/Building a Renderer/3 TokamakStatic Setup.md index 496e26e03..e8059e956 100644 --- a/docs/Building a Renderer/3 TokamakStatic Setup.md +++ b/docs/Building a Renderer/3 TokamakStatic Setup.md @@ -1,5 +1,6 @@ -# `TokamakStatic` Setup -Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to use. A `Core.swift` file is used to reexport these symbols. For `TokamakStatic`, we’ll use the following `Core.swift` file: +# `TokamakStaticHTML` Setup + +Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, we’ll use the following `Core.swift` file: ```swift import TokamakCore diff --git a/docs/Building a Renderer/4 Building the Target.md b/docs/Building a Renderer/4 Building the Target.md index 8b8ab5912..684716c9a 100644 --- a/docs/Building a Renderer/4 Building the Target.md +++ b/docs/Building a Renderer/4 Building the Target.md @@ -1,12 +1,13 @@ # Building the `Target` + If you recall, we defined a `Target` as: > the destination for rendered `Views` -In `TokamakStatic`, this would be a tag in an `HTML` file. A tag has several properties, although we don’t need to worry about all of them. For now, we can consider a tag to have: +In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties, although we don’t need to worry about all of them. For now, we can consider a tag to have: -* The HTML for the tag itself (outer HTML) -* Child tags (inner HTML) +- The HTML for the tag itself (outer HTML) +- Child tags (inner HTML) We can describe our target simply: diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md index 88bb25822..15a74c7f8 100644 --- a/docs/Building a Renderer/5 Building the Renderer.md +++ b/docs/Building a Renderer/5 Building the Renderer.md @@ -77,7 +77,7 @@ public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTa 2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). 3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it. -The other two functions required by the `Renderer` protocol can crash, as `TokamakStatic` doesn’t support state changes: +The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML` doesn’t support state changes: ```swift public func update(target: HTMLTarget, with host: MountedHost) { diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md index 3fa90cdc9..3934ec33e 100644 --- a/docs/Building a Renderer/6 Providing platform-specific primitives.md +++ b/docs/Building a Renderer/6 Providing platform-specific primitives.md @@ -1,7 +1,8 @@ # Providing platform-specific primitives -Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the `StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. -This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our `Renderer` understands. For instance, `TokamakDOM` (and `TokamakStatic` by extension) use the `HTML` view. Let’s look at a simpler version of this view: +Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the `StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. + +This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our `Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the `HTML` view. Let’s look at a simpler version of this view: ```swift protocol AnyHTML { @@ -20,7 +21,7 @@ struct HTML: View, AnyHTML { } ``` -Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without worrying about the `associatedtypes` involved with `View`. +Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without worrying about the `associatedtypes` involved with `View`. ## `ViewDeferredToRenderer` @@ -47,6 +48,7 @@ guard let html = mapAnyView( Then we were able to access the properties of the HTML. ## Proxies + Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to access the storage of the `Text` view, we were required to use a `_TextProxy`. Proxies contain all of the properties of the primitive necessary to build your platform-specific implementation. From 43b61e8adfd543af0a986d9471e2fed62d07a6a2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 1 Aug 2020 20:22:07 +0100 Subject: [PATCH 09/10] Make module naming in error messages consistent --- Sources/TokamakStaticHTML/StaticRenderer.swift | 8 ++++---- docs/Building a Renderer/5 Building the Renderer.md | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/TokamakStaticHTML/StaticRenderer.swift b/Sources/TokamakStaticHTML/StaticRenderer.swift index 927490577..4dfea447b 100644 --- a/Sources/TokamakStaticHTML/StaticRenderer.swift +++ b/Sources/TokamakStaticHTML/StaticRenderer.swift @@ -83,7 +83,7 @@ public final class StaticHTMLRenderer: Renderer { environment: EnvironmentValues(), renderer: self, scheduler: { _ in - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } ) } @@ -97,7 +97,7 @@ public final class StaticHTMLRenderer: Renderer { environment: EnvironmentValues(), renderer: self, scheduler: { _ in - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } ) } @@ -121,7 +121,7 @@ public final class StaticHTMLRenderer: Renderer { } public func update(target: HTMLTarget, with host: MountedHost) { - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } public func unmount( @@ -130,6 +130,6 @@ public final class StaticHTMLRenderer: Renderer { with host: MountedHost, completion: @escaping () -> () ) { - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } } diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md index 15a74c7f8..9175c3e29 100644 --- a/docs/Building a Renderer/5 Building the Renderer.md +++ b/docs/Building a Renderer/5 Building the Renderer.md @@ -29,7 +29,7 @@ public init(_ view: V) { renderer: self, environment: EnvironmentValues() ) { closure in - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } } ``` @@ -81,7 +81,7 @@ The other two functions required by the `Renderer` protocol can crash, as `Tokam ```swift public func update(target: HTMLTarget, with host: MountedHost) { - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } public func unmount( @@ -90,7 +90,7 @@ public func unmount( with host: MountedHost, completion: @escaping () -> () ) { - fatalError("Stateful apps cannot be created with TokamakStatic") + fatalError("Stateful apps cannot be created with TokamakStaticHTML") } ``` From 87199c7a3306b22875a1555c25bfdc32034450a0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 1 Aug 2020 20:24:09 +0100 Subject: [PATCH 10/10] Update line length in markdown files --- .../1 Renderers in Tokamak.md | 17 +++++++----- .../2 Understanding Renderers.md | 17 ++++++++---- .../3 TokamakStatic Setup.md | 4 ++- .../4 Building the Target.md | 6 +++-- .../5 Building the Renderer.md | 27 ++++++++++++------- ... Providing platform-specific primitives.md | 17 ++++++++---- 6 files changed, 60 insertions(+), 28 deletions(-) diff --git a/docs/Building a Renderer/1 Renderers in Tokamak.md b/docs/Building a Renderer/1 Renderers in Tokamak.md index 88fc2a663..c2f0af4bf 100644 --- a/docs/Building a Renderer/1 Renderers in Tokamak.md +++ b/docs/Building a Renderer/1 Renderers in Tokamak.md @@ -1,14 +1,19 @@ # `Renderers` in Tokamak -Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use to construct a representation of `Views` that your platform understands. +Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use +to construct a representation of `Views` that your platform understands. -To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which you can find in the `Tokamak` repository). +To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which +you can find in the `Tokamak` repository). Before we create the `Renderer`, we need to understand the requirements of our platform: -1. Stateful apps cannot be created - This simplifies the scope of our project, as we only have to render once. However, if you are building a `Renderer` that supports state changes, the process is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a `View` has to be redrawn. -2. HTML should be rendered - `TokamakDOM` provides HTML representations of many `Views`, so we can utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can understand, and when you are required to do so. +1. Stateful apps cannot be created This simplifies the scope of our project, as we only have to + render once. However, if you are building a `Renderer` that supports state changes, the process + is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a + `View` has to be redrawn. +2. HTML should be rendered `TokamakDOM` provides HTML representations of many `Views`, so we can + utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can + understand, and when you are required to do so. And that’s it! In the next part we’ll go more in depth on `Renderers`. diff --git a/docs/Building a Renderer/2 Understanding Renderers.md b/docs/Building a Renderer/2 Understanding Renderers.md index 64bc3b08f..23af81be6 100644 --- a/docs/Building a Renderer/2 Understanding Renderers.md +++ b/docs/Building a Renderer/2 Understanding Renderers.md @@ -1,10 +1,17 @@ # Understanding `Renderers` + So, what goes into a `Renderer`? -1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is `UIView`, on macOS an `NSView`, and on the web we render to DOM nodes. -2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It notifies your `Renderer` of what views need to be mounted/unmounted. -3. `func mountTarget`- This function is called when a new target instance should be created and added to the parent (either as a subview or some other way, e.g. installed if it’s a layout constraint). -4. `func update` - This function is called when an existing target instance should be updated (e.g. when `State` changes). -5. `func unmount` - This function is called when an existing target instance should be unmounted: removed from the parent and most likely destroyed. +1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is + `UIView`, on macOS an `NSView`, and on the web we render to DOM nodes. +2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It + notifies your `Renderer` of what views need to be mounted/unmounted. +3. `func mountTarget`- This function is called when a new target instance should be created and + added to the parent (either as a subview or some other way, e.g. installed if it’s a layout + constraint). +4. `func update` - This function is called when an existing target instance should be updated (e.g. + when `State` changes). +5. `func unmount` - This function is called when an existing target instance should be unmounted: + removed from the parent and most likely destroyed. That’s it! Let’s get our project setup. diff --git a/docs/Building a Renderer/3 TokamakStatic Setup.md b/docs/Building a Renderer/3 TokamakStatic Setup.md index e8059e956..09aaab331 100644 --- a/docs/Building a Renderer/3 TokamakStatic Setup.md +++ b/docs/Building a Renderer/3 TokamakStatic Setup.md @@ -1,6 +1,8 @@ # `TokamakStaticHTML` Setup -Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, we’ll use the following `Core.swift` file: +Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to +use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, we’ll use the +following `Core.swift` file: ```swift import TokamakCore diff --git a/docs/Building a Renderer/4 Building the Target.md b/docs/Building a Renderer/4 Building the Target.md index 684716c9a..c5a0be1f8 100644 --- a/docs/Building a Renderer/4 Building the Target.md +++ b/docs/Building a Renderer/4 Building the Target.md @@ -4,7 +4,8 @@ If you recall, we defined a `Target` as: > the destination for rendered `Views` -In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties, although we don’t need to worry about all of them. For now, we can consider a tag to have: +In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties, +although we don’t need to worry about all of them. For now, we can consider a tag to have: - The HTML for the tag itself (outer HTML) - Child tags (inner HTML) @@ -24,7 +25,8 @@ public final class HTMLTarget: Target { } ``` -`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View` it hosts, the `HTML` that represents it, and its child elements. +`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View` +it hosts, the `HTML` that represents it, and its child elements. Lastly, we can also provide an HTML string representation of the target: diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md index 9175c3e29..aeb0e5fab 100644 --- a/docs/Building a Renderer/5 Building the Renderer.md +++ b/docs/Building a Renderer/5 Building the Renderer.md @@ -4,7 +4,7 @@ Now that we have a `Target`, we can start the `Renderer`: ```swift public final class StaticHTMLRenderer: Renderer { - public private(set) var reconciler: StackReconciler? + public private(set) var reconciler: StackReconciler? var rootTarget: HTMLTarget public var html: String { @@ -17,7 +17,8 @@ public final class StaticHTMLRenderer: Renderer { } ``` -We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus on mounting and un-mounting `Views`. +We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus +on mounting and un-mounting `Views`. ```swift ... @@ -34,7 +35,10 @@ public init(_ view: V) { } ``` -Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the `View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default `EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the reconciler when it can update. In this case, we won’t need to update, so we can crash. +Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the +`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default +`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the +reconciler when it can update. In this case, we won’t need to update, so we can crash. `HTMLBody` is declared like so: @@ -53,7 +57,7 @@ Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it ```swift public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { - // 1. + // 1. guard let html = mapAnyView( host.view, transform: { (html: AnyHTML) in html } @@ -73,11 +77,14 @@ public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTa }} ``` -1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be used with our `HTMLTarget`. -2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). +1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be + used with our `HTMLTarget`. +2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no + representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). 3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it. -The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML` doesn’t support state changes: +The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML` +doesn’t support state changes: ```swift public func update(target: HTMLTarget, with host: MountedHost) { @@ -97,7 +104,8 @@ public func unmount( If you are creating a `Renderer` that supports state changes, here’s a quick synopsis: - `func update` - Mutate the `target` to match the `host`. -- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been removed. +- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been + removed. Now that we can mount, let’s give it a try: @@ -122,4 +130,5 @@ This spits out: ``` -Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll bring Tokamak to. +Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll +bring Tokamak to. diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md index 3934ec33e..165e21f26 100644 --- a/docs/Building a Renderer/6 Providing platform-specific primitives.md +++ b/docs/Building a Renderer/6 Providing platform-specific primitives.md @@ -1,8 +1,11 @@ # Providing platform-specific primitives -Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the `StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. +Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the +`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. -This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our `Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the `HTML` view. Let’s look at a simpler version of this view: +This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our +`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the +`HTML` view. Let’s look at a simpler version of this view: ```swift protocol AnyHTML { @@ -21,7 +24,9 @@ struct HTML: View, AnyHTML { } ``` -Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without worrying about the `associatedtypes` involved with `View`. +Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also +conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without +worrying about the `associatedtypes` involved with `View`. ## `ViewDeferredToRenderer` @@ -49,6 +54,8 @@ Then we were able to access the properties of the HTML. ## Proxies -Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to access the storage of the `Text` view, we were required to use a `_TextProxy`. +Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to +access the storage of the `Text` view, we were required to use a `_TextProxy`. -Proxies contain all of the properties of the primitive necessary to build your platform-specific implementation. +Proxies contain all of the properties of the primitive necessary to build your platform-specific +implementation.