Replace `ViewDeferredToRenderer`, fix renderer tests (#408)

This allows writing tests for `TokamakStaticHTML`, `TokamakDOM`, and `TokamakGTK` targets.

The issue was caused by conflicting `ViewDeferredToRenderer` conformances declared in different modules, including the `TokamakTestRenderer` module. 

This works around a general limitation in Swift, which was [discussed at length on Swift Forums previously](https://forums.swift.org/t/an-implementation-model-for-rational-protocol-conformance-behavior/37171). When multiple conflicting conformances to the same protocol (`ViewDeferredToRenderer` in our case) exist in different modules, only one of them is available in a given binary (even a test binary). Also, only of them can be loaded and used. Which one exactly is loaded can't be known at compile-time, which is hard to debug and leads to breaking tests that cover code in different renderers. We had to disable `TokamakStaticHTMLTests` for this reason.

The workaround is to declare two new functions in the `Renderer` protocol:

```swift
public protocol Renderer: AnyObject {
  // ...
  // Functions unrelated to the issue at hand skipped for brevity.

  /** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
   this renderer.
   */
  func body(for view: Any) -> AnyView?

  /** Returns `true` if a given view type is a primitive view that should be deferred to this
   renderer.
   */
  func isPrimitiveView(_ type: Any.Type) -> Bool
}
```

Now each renderer can declare their own protocols for their primitive views, i.e. `HTMLPrimitive`, `DOMPrimitive`, `GTKPrimitive` etc, delegating to them from the implementations of `body(for view:)` and `isPrimitiveView(_:)`. Conformances to these protocols can't conflict across different modules. Also, these protocols can have `internal` visibility, as opposed to `ViewDeferredToRenderer`, which had to be declared as `public` in `TokamakCore` to be visible in renderer modules.
This commit is contained in:
Max Desiatov 2021-06-07 17:24:02 +01:00 committed by GitHub
parent da9843d07f
commit 5926e9f182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 930 additions and 648 deletions

View File

@ -172,14 +172,9 @@ let package = Package(
name: "TokamakTests",
dependencies: ["TokamakTestRenderer"]
),
// FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved
// Currently, when multiple modules that have conflicting `ViewDeferredToRenderer`
// implementations are linked in the same binary, only a single one is used with no defined
// behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution
// that isn't prone to these hard to debug errors.
// .testTarget(
// name: "TokamakStaticHTMLTests",
// dependencies: ["TokamakStaticHTML"]
// ),
.testTarget(
name: "TokamakStaticHTMLTests",
dependencies: ["TokamakStaticHTML"]
),
]
)

View File

@ -30,7 +30,7 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
// They also have no parents, so the `parent` argument is discarded as well.
let childBody = reconciler.render(mountedApp: self)
let child: MountedElement<R> = mountChild(childBody)
let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
mountedChildren = [child]
child.mount(before: nil, on: self, with: reconciler)
}
@ -39,9 +39,14 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
mountedChildren.forEach { $0.unmount(with: reconciler) }
}
private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> {
/// Mounts a child scene within the app.
/// - Parameters:
/// - renderer: A instance conforming to the `Renderer` protocol to render the mounted scene with.
/// - childBody: The body of the child scene to mount for this app.
/// - Returns: Returns an instance of the `MountedScene` class that's already mounted in this app.
private func mountChild(_ renderer: R, _ childBody: _AnyScene) -> MountedScene<R> {
let mountedScene: MountedScene<R> = childBody
.makeMountedScene(parentTarget, environmentValues, self)
.makeMountedScene(renderer, parentTarget, environmentValues, self)
if let title = mountedScene.title {
// swiftlint:disable force_cast
(app.type as! _TitledApp.Type)._setTitle(title)
@ -59,7 +64,7 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
$0.environmentValues = environmentValues
$0.scene = _AnyScene(element)
},
mountChild: { mountChild($0) }
mountChild: { mountChild(reconciler.renderer, $0) }
)
}
}

View File

@ -26,6 +26,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
let childBody = reconciler.render(compositeView: self)
let child: MountedElement<R> = childBody.makeMountedView(
reconciler.renderer,
parentTarget,
environmentValues,
self
@ -33,7 +34,8 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler)
// `_TargetRef` is a composite view, so it's enough to check for it only here
// `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite view, so it's enough
// to check for it only here.
if var targetRef = view.view as? TargetRefType {
// `_TargetRef` body is not always a host view that has a target, need to traverse
// all descendants to find a `MountedHostView<R>` instance.
@ -51,7 +53,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
reconciler.afterCurrentRender(perform: { [weak self] in
guard let self = self else { return }
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
// FIXME: this has to be implemented in a renderer-specific way, otherwise it's equivalent to
// `_onMount` and `_onUnmount` at the moment,
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
if let appearanceAction = self.view.view as? AppearanceActionType {
@ -89,7 +91,9 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
$0.environmentValues = environmentValues
$0.view = AnyView(element)
},
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
mountChild: {
$0.makeMountedView(reconciler.renderer, parentTarget, environmentValues, self)
}
)
}
}

View File

@ -222,13 +222,14 @@ extension TypeInfo {
extension AnyView {
func makeMountedView<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
if type == EmptyView.self {
return MountedEmptyView(self, environmentValues, parent)
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
} else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
return MountedHostView(self, parentTarget, environmentValues, parent)
} else {
return MountedCompositeView(self, parentTarget, environmentValues, parent)

View File

@ -45,7 +45,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
guard let target = reconciler.renderer?.mountTarget(
guard let target = reconciler.renderer.mountTarget(
before: sibling,
to: parentTarget,
with: self
@ -57,7 +57,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
guard !view.children.isEmpty else { return }
mountedChildren = view.children.map {
$0.makeMountedView(target, environmentValues, self)
$0.makeMountedView(reconciler.renderer, target, environmentValues, self)
}
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
@ -74,7 +74,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
override func unmount(with reconciler: StackReconciler<R>) {
guard let target = target else { return }
reconciler.renderer?.unmount(
reconciler.renderer.unmount(
target: target,
from: parentTarget,
with: self
@ -88,7 +88,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
updateEnvironment()
target.view = view
reconciler.renderer?.update(target: target, with: self)
reconciler.renderer.update(target: target, with: self)
var childrenViews = view.children
@ -101,7 +101,9 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
// if no existing children then mount all new children
case (true, false):
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) }
mountedChildren = childrenViews.map {
$0.makeMountedView(reconciler.renderer, target, environmentValues, self)
}
mountedChildren.forEach { $0.mount(on: self, with: reconciler) }
// if both arrays have items then reconcile by types and keys
@ -124,7 +126,12 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
by unmounting it.
*/
newChild = childView.makeMountedView(target, environmentValues, self)
newChild = childView.makeMountedView(
reconciler.renderer,
target,
environmentValues,
self
)
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler)
mountedChild.unmount(with: reconciler)
}
@ -144,7 +151,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
// mount remaining views
for firstChild in childrenViews {
let newChild: MountedElement<R> =
firstChild.makeMountedView(target, environmentValues, self)
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, self)
newChild.mount(on: self, with: reconciler)
newChildren.append(newChild)
}

View File

@ -36,7 +36,7 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
let childBody = reconciler.render(mountedScene: self)
let child: MountedElement<R> = childBody
.makeMountedElement(parentTarget, environmentValues, self)
.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler)
}
@ -60,7 +60,9 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
$0.view = AnyView(view)
}
},
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) }
mountChild: {
$0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
}
)
}
}
@ -76,21 +78,23 @@ extension _AnyScene.BodyResult {
}
func makeMountedElement<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
switch self {
case let .scene(scene):
return scene.makeMountedScene(parentTarget, environmentValues, parent)
return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent)
case let .view(view):
return view.makeMountedView(parentTarget, environmentValues, parent)
return view.makeMountedView(renderer, parentTarget, environmentValues, parent)
}
}
}
extension _AnyScene {
func makeMountedScene<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
@ -104,11 +108,16 @@ extension _AnyScene {
let children: [MountedElement<R>]
if let deferredScene = scene as? SceneDeferredToRenderer {
children = [
deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent),
deferredScene.deferredBody.makeMountedView(
renderer,
parentTarget,
environmentValues,
parent
),
]
} else if let groupScene = scene as? GroupScene {
children = groupScene.children.map {
$0.makeMountedScene(parentTarget, environmentValues, parent)
$0.makeMountedScene(renderer, parentTarget, environmentValues, parent)
}
} else {
children = []

View File

@ -66,4 +66,14 @@ public protocol Renderer: AnyObject {
with host: MountedHost,
completion: @escaping () -> ()
)
/** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
this renderer.
*/
func primitiveBody(for view: Any) -> AnyView?
/** Returns `true` if a given view type is a primitive view that should be deferred to this
renderer.
*/
func isPrimitiveView(_ type: Any.Type) -> Bool
}

View File

@ -46,7 +46,7 @@ public struct FillStyle: Equatable, ShapeStyle {
}
}
public struct _ShapeView<Content, Style>: PrimitiveView where Content: Shape, Style: ShapeStyle {
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
@Environment(\.self) public var environment
@Environment(\.foregroundColor) public var foregroundColor
public var shape: Content

View File

@ -18,7 +18,7 @@
import CombineShim
/** A class that reconciles a "raw" tree of element values (such as `App`, `Scene` and `View`,
all coming from `body` or `deferredBody` properties) with a tree of mounted element instances
all coming from `body` or `renderedBody` properties) with a tree of mounted element instances
('MountedApp', `MountedScene`, `MountedCompositeView` and `MountedHostView` respectively). Any
updates to the former tree are reflected in the latter tree, and then resulting changes are
delegated to the renderer for it to reflect those in its viewport.
@ -52,7 +52,7 @@ public final class StackReconciler<R: Renderer> {
/** A renderer instance to delegate to. Usually the renderer owns the reconciler instance, thus
the reference has to be weak to avoid a reference cycle.
**/
private(set) weak var renderer: R?
private(set) unowned var renderer: R
/** A platform-specific implementation of an event loop scheduler. Usually reconciler
updates are scheduled in reponse to user input. To make updates non-blocking so that the app
@ -73,7 +73,7 @@ public final class StackReconciler<R: Renderer> {
self.scheduler = scheduler
rootTarget = target
rootElement = AnyView(view).makeMountedView(target, environment, nil)
rootElement = AnyView(view).makeMountedView(renderer, target, environment, nil)
performInitialMount()
}
@ -201,45 +201,50 @@ public final class StackReconciler<R: Renderer> {
}.store(in: &mountedApp.persistentSubscriptions)
}
private func render<T>(
compositeElement: MountedCompositeElement<R>,
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>
) -> T {
private func body(
of compositeElement: MountedCompositeElement<R>,
keyPath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>
) -> Any {
compositeElement.updateEnvironment()
if let info = typeInfo(of: compositeElement.type) {
var stateIdx = 0
let dynamicProps = info.dynamicProperties(
&compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath]
source: &compositeElement[keyPath: keyPath]
)
compositeElement.transientSubscriptions = []
for property in dynamicProps {
// Setup state/subscriptions
if property.type is ValueStorage.Type {
setupStorage(id: stateIdx, for: property, of: compositeElement, body: bodyKeypath)
setupStorage(id: stateIdx, for: property, of: compositeElement, body: keyPath)
stateIdx += 1
}
if property.type is ObservedProperty.Type {
setupTransientSubscription(for: property, of: compositeElement, body: bodyKeypath)
setupTransientSubscription(for: property, of: compositeElement, body: keyPath)
}
}
}
return compositeElement[keyPath: result](compositeElement[keyPath: bodyKeypath])
return compositeElement[keyPath: keyPath]
}
func render(compositeView: MountedCompositeView<R>) -> AnyView {
render(compositeElement: compositeView, body: \.view.view, result: \.view.bodyClosure)
let view = body(of: compositeView, keyPath: \.view.view)
guard let renderedBody = renderer.primitiveBody(for: view) else {
return compositeView.view.bodyClosure(view)
}
return renderedBody
}
func render(mountedApp: MountedApp<R>) -> _AnyScene {
render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure)
mountedApp.app.bodyClosure(body(of: mountedApp, keyPath: \.app.app))
}
func render(mountedScene: MountedScene<R>) -> _AnyScene.BodyResult {
render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure)
mountedScene.scene.bodyClosure(body(of: mountedScene, keyPath: \.scene.scene))
}
func reconcile<Element>(

View File

@ -12,10 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/// A helper protocol for erasing generic parameters of the `_TargetRef` type.
protocol TargetRefType {
var target: Target? { get set }
}
/** Allows capturing target instance of aclosest descendant host view. The resulting instance
is written to a given `binding`. The actual assignment to this binding is done within the
`MountedCompositeView` implementation. */
public struct _TargetRef<V: View, T>: View, TargetRefType {
let binding: Binding<T?>
@ -31,8 +35,9 @@ public struct _TargetRef<V: View, T>: View, TargetRefType {
}
public extension View {
/** Allows capturing target instance of aclosest descendant host view. The resulting instance
is written to a given `binding`. */
/** A modifier that returns a `_TargetRef` value, which captures a target instance of a
closest descendant host view.
The resulting instance is written to a given `binding`. */
@_spi(TokamakCore)
func _targetRef<T: Target>(_ binding: Binding<T?>) -> _TargetRef<Self, T> {
.init(binding: binding, view: self)

View File

@ -15,7 +15,7 @@
// Created by Gene Z. Ragan on 07/22/2020.
public struct ButtonStyleConfiguration {
public struct Label: PrimitiveView {
public struct Label: _PrimitiveView {
let content: AnyView
}

View File

@ -299,7 +299,7 @@ public extension Color {
static let secondary: Self = .init(systemColor: .secondary)
static let accentColor: Self = .init(_EnvironmentDependentColorBox {
($0.accentColor ?? Self.blue)
$0.accentColor ?? Self.blue
})
init(_ color: UIColor) {

View File

@ -17,8 +17,8 @@
public enum TextAlignment: Hashable, CaseIterable {
case leading,
center,
trailing
center,
trailing
}
extension EnvironmentValues {

View File

@ -16,7 +16,7 @@
//
/// A type-erased view.
public struct AnyView: PrimitiveView {
public struct AnyView: _PrimitiveView {
/// The type of the underlying `view`.
let type: Any.Type
@ -50,21 +50,8 @@ public struct AnyView: PrimitiveView {
bodyType = V.Body.self
self.view = view
if view is ViewDeferredToRenderer {
bodyClosure = {
let deferredView: Any
if let opt = $0 as? AnyOptional, let value = opt.value {
deferredView = value
} else {
deferredView = $0
}
// swiftlint:disable:next force_cast
return (deferredView as! ViewDeferredToRenderer).deferredBody
}
} else {
// swiftlint:disable:next force_cast
bodyClosure = { AnyView(($0 as! V).body) }
}
// swiftlint:disable:next force_cast
bodyClosure = { AnyView(($0 as! V).body) }
}
}
}

View File

@ -48,7 +48,7 @@ public struct Button<Label>: View where Label: View {
}
}
public struct _Button<Label>: PrimitiveView where Label: View {
public struct _Button<Label>: _PrimitiveView where Label: View {
public let label: Label
public let action: () -> ()
@State public var isPressed = false

View File

@ -17,7 +17,7 @@
import struct Foundation.URL
public struct Link<Label>: PrimitiveView where Label: View {
public struct Link<Label>: _PrimitiveView where Label: View {
let destination: URL
let label: Label

View File

@ -15,7 +15,7 @@
// Created by Carson Katri on 7/3/20.
//
public struct DisclosureGroup<Label, Content>: PrimitiveView where Label: View, Content: View {
public struct DisclosureGroup<Label, Content>: _PrimitiveView where Label: View, Content: View {
@State var isExpanded: Bool = false
let isExpandedBinding: Binding<Bool>?

View File

@ -31,7 +31,7 @@ protocol ForEachProtocol: GroupView {
/// Text("\($0)")
/// }
/// }
public struct ForEach<Data, ID, Content>: PrimitiveView where Data: RandomAccessCollection,
public struct ForEach<Data, ID, Content>: _PrimitiveView where Data: RandomAccessCollection,
ID: Hashable,
Content: View
{

View File

@ -19,7 +19,7 @@ public struct Group<Content> {
}
}
extension Group: PrimitiveView & View where Content: View {}
extension Group: _PrimitiveView & View where Content: View {}
extension Group: ParentView where Content: View {
@_spi(TokamakCore)

View File

@ -1,401 +0,0 @@
// Copyright 2018-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/2/20.
//
public struct List<SelectionValue, Content>: View
where SelectionValue: Hashable, Content: View
{
public enum _Selection {
case one(Binding<SelectionValue?>?)
case many(Binding<Set<SelectionValue>>?)
}
let selection: _Selection
let content: Content
@Environment(\.listStyle) var style
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content) {
self.selection = .many(selection)
self.content = content()
}
public init(selection: Binding<SelectionValue?>?, @ViewBuilder content: () -> Content) {
self.selection = .one(selection)
self.content = content()
}
var listStack: some View {
VStack(alignment: .leading) { () -> AnyView in
if let contentContainer = content as? ParentView {
var sections = [AnyView]()
var currentSection = [AnyView]()
for child in contentContainer.children {
if child.view is SectionView {
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in view }
}))
currentSection = []
}
sections.append(child)
} else {
if child.children.count > 0 {
currentSection.append(contentsOf: child.children)
} else {
currentSection.append(child)
}
}
}
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in view }
}))
}
return AnyView(_ListRow.buildItems(sections) { view, isLast in
if let section = view.view as? SectionView {
section.listRow(style)
} else {
_ListRow.listRow(view, style, isLast: isLast)
}
})
} else {
return AnyView(content)
}
}
}
@_spi(TokamakCore)
public var body: some View {
if let style = style as? ListStyleDeferredToRenderer {
style.listBody(ScrollView {
HStack { Spacer() }
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
})
.frame(minHeight: 0, maxHeight: .infinity)
} else {
ScrollView {
HStack { Spacer() }
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
}
}
}
}
public enum _ListRow {
static func buildItems<RowView>(
_ children: [AnyView],
@ViewBuilder rowView: @escaping (AnyView, Bool) -> RowView
) -> some View where RowView: View {
ForEach(Array(children.enumerated()), id: \.offset) { offset, view in
VStack(alignment: .leading) {
HStack { Spacer() }
rowView(view, offset == children.count - 1)
}
}
}
@ViewBuilder
public static func listRow<V: View>(_ view: V, _ style: ListStyle, isLast: Bool) -> some View {
(style as? ListStyleDeferredToRenderer)?.listRow(view) ??
AnyView(view.padding([.trailing, .top, .bottom]))
if !isLast && style.hasDividers {
Divider()
}
}
}
/// This is a helper type that works around absence of "package private" access control in Swift
public struct _ListProxy<SelectionValue, Content>
where SelectionValue: Hashable, Content: View
{
public let subject: List<SelectionValue, Content>
public init(_ subject: List<SelectionValue, Content>) {
self.subject = subject
}
public var content: Content { subject.content }
public var selection: List<SelectionValue, Content>._Selection { subject.selection }
}
public extension List {
// - MARK: Collection initializers
init<Data, RowContent>(
_ data: Data,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View,
Data.Element: Identifiable
{
self.init(selection: selection) { ForEach(data) { row in HStack { rowContent(row) } } }
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection,
ID: Hashable, RowContent: View
{
self.init(selection: selection) { ForEach(data, id: id) { row in HStack { rowContent(row) } } }
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
ForEach(data, id: id) { row in
HStack { rowContent(row) }
}
}
}
init<Data, RowContent>(
_ data: Data,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
// - MARK: Range initializers
init<RowContent>(
_ data: Range<Int>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
self.init(selection: selection) {
ForEach(data) { row in
HStack { rowContent(row) }
}
}
}
init<RowContent>(
_ data: Range<Int>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
self.init(selection: selection) {
ForEach(data) { row in
HStack { rowContent(row) }
}
}
}
// - MARK: OutlineGroup initializers
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
}
public extension List where SelectionValue == Never {
init(@ViewBuilder content: () -> Content) {
selection = .one(nil)
self.content = content()
}
init<Data, RowContent>(
_ data: Data,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
selection = .one(nil)
content = ForEach(data) { row in
HStack { rowContent(row) }
}
}
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
selection = .one(nil)
content = ForEach(data, id: id) { row in
HStack { rowContent(row) }
}
}
init<RowContent>(
_ data: Range<Int>,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
selection = .one(nil)
content = ForEach(data) { row in
HStack { rowContent(row) }
}
}
}

View File

@ -0,0 +1,195 @@
// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Max Desiatov on 06/06/2021.
//
public extension List {
// - MARK: Collection initializers
init<Data, RowContent>(
_ data: Data,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View,
Data.Element: Identifiable
{
self.init(selection: selection) { ForEach(data) { row in HStack { rowContent(row) } } }
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection,
ID: Hashable, RowContent: View
{
self.init(selection: selection) { ForEach(data, id: id) { row in HStack { rowContent(row) } } }
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
ForEach(data, id: id) { row in
HStack { rowContent(row) }
}
}
}
init<Data, RowContent>(
_ data: Data,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
// - MARK: Range initializers
init<RowContent>(
_ data: Range<Int>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
self.init(selection: selection) {
ForEach(data) { row in
HStack { rowContent(row) }
}
}
}
init<RowContent>(
_ data: Range<Int>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
self.init(selection: selection) {
ForEach(data) { row in
HStack { rowContent(row) }
}
}
}
// - MARK: OutlineGroup initializers
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
}

View File

@ -0,0 +1,103 @@
// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Max Desiatov on 06/06/2021.
//
public extension List where SelectionValue == Never {
init(@ViewBuilder content: () -> Content) {
selection = .one(nil)
self.content = content()
}
init<Data, RowContent>(
_ data: Data,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
selection = .one(nil)
content = ForEach(data) { row in
HStack { rowContent(row) }
}
}
init<Data, RowContent>(
_ data: Data,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable
{
self.init {
OutlineGroup(data, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == OutlineGroup<
Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>
>, Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
self.init {
OutlineGroup(data, id: id, children: children) { row in
HStack { rowContent(row) }
}
}
}
init<Data, ID, RowContent>(
_ data: Data,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent
)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View
{
selection = .one(nil)
content = ForEach(data, id: id) { row in
HStack { rowContent(row) }
}
}
init<RowContent>(
_ data: Range<Int>,
@ViewBuilder rowContent: @escaping (Int) -> RowContent
)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View
{
selection = .one(nil)
content = ForEach(data) { row in
HStack { rowContent(row) }
}
}
}

View File

@ -0,0 +1,135 @@
// Copyright 2018-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/2/20.
//
public struct List<SelectionValue, Content>: View
where SelectionValue: Hashable, Content: View
{
public enum _Selection {
case one(Binding<SelectionValue?>?)
case many(Binding<Set<SelectionValue>>?)
}
let selection: _Selection
let content: Content
@Environment(\.listStyle) var style
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content) {
self.selection = .many(selection)
self.content = content()
}
public init(selection: Binding<SelectionValue?>?, @ViewBuilder content: () -> Content) {
self.selection = .one(selection)
self.content = content()
}
var listStack: some View {
VStack(alignment: .leading) { () -> AnyView in
if let contentContainer = content as? ParentView {
var sections = [AnyView]()
var currentSection = [AnyView]()
for child in contentContainer.children {
if child.view is SectionView {
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in view }
}))
currentSection = []
}
sections.append(child)
} else {
if child.children.count > 0 {
currentSection.append(contentsOf: child.children)
} else {
currentSection.append(child)
}
}
}
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in view }
}))
}
return AnyView(_ListRow.buildItems(sections) { view, isLast in
if let section = view.view as? SectionView {
section.listRow(style)
} else {
_ListRow.listRow(view, style, isLast: isLast)
}
})
} else {
return AnyView(content)
}
}
}
@_spi(TokamakCore)
public var body: some View {
if let style = style as? ListStyleDeferredToRenderer {
style.listBody(ScrollView {
HStack { Spacer() }
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
})
.frame(minHeight: 0, maxHeight: .infinity)
} else {
ScrollView {
HStack { Spacer() }
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
}
}
}
}
public enum _ListRow {
static func buildItems<RowView>(
_ children: [AnyView],
@ViewBuilder rowView: @escaping (AnyView, Bool) -> RowView
) -> some View where RowView: View {
ForEach(Array(children.enumerated()), id: \.offset) { offset, view in
VStack(alignment: .leading) {
HStack { Spacer() }
rowView(view, offset == children.count - 1)
}
}
}
@ViewBuilder
public static func listRow<V: View>(_ view: V, _ style: ListStyle, isLast: Bool) -> some View {
(style as? ListStyleDeferredToRenderer)?.listRow(view) ??
AnyView(view.padding([.trailing, .top, .bottom]))
if !isLast && style.hasDividers {
Divider()
}
}
}
/// This is a helper type that works around absence of "package private" access control in Swift
public struct _ListProxy<SelectionValue, Content>
where SelectionValue: Hashable, Content: View
{
public let subject: List<SelectionValue, Content>
public init(_ subject: List<SelectionValue, Content>) {
self.subject = subject
}
public var content: Content { subject.content }
public var selection: List<SelectionValue, Content>._Selection { subject.selection }
}

View File

@ -18,7 +18,7 @@
/// A `View` created from a `Tuple` of `View` values.
///
/// Mainly for use with `@ViewBuilder`.
public struct TupleView<T>: PrimitiveView {
public struct TupleView<T>: _PrimitiveView {
public let value: T
let _children: [AnyView]

View File

@ -17,7 +17,7 @@
import Foundation
public struct Image: PrimitiveView {
public struct Image: _PrimitiveView {
let label: Text?
let name: String
let bundle: Bundle?

View File

@ -40,7 +40,7 @@ public func makeProxy(from size: CGSize) -> GeometryProxy {
// public subscript<T>(anchor: Anchor<T>) -> T {}
// }
public struct GeometryReader<Content>: PrimitiveView where Content: View {
public struct GeometryReader<Content>: _PrimitiveView where Content: View {
public let content: (GeometryProxy) -> Content
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content

View File

@ -28,7 +28,7 @@ public enum VerticalAlignment: Equatable {
/// Text("Hello")
/// Text("World")
/// }
public struct HStack<Content>: PrimitiveView where Content: View {
public struct HStack<Content>: _PrimitiveView where Content: View {
public let alignment: VerticalAlignment
public let spacing: CGFloat?
public let content: Content

View File

@ -15,7 +15,7 @@
// Created by Carson Katri on 7/13/20.
//
public struct LazyHGrid<Content>: PrimitiveView where Content: View {
public struct LazyHGrid<Content>: _PrimitiveView where Content: View {
let rows: [GridItem]
let alignment: VerticalAlignment
let spacing: CGFloat

View File

@ -15,7 +15,7 @@
// Created by Carson Katri on 7/13/20.
//
public struct LazyVGrid<Content>: PrimitiveView where Content: View {
public struct LazyVGrid<Content>: _PrimitiveView where Content: View {
let columns: [GridItem]
let alignment: HorizontalAlignment
let spacing: CGFloat

View File

@ -35,7 +35,7 @@
/// Text("\($0)")
/// }
/// }
public struct ScrollView<Content>: PrimitiveView where Content: View {
public struct ScrollView<Content>: _PrimitiveView where Content: View {
public let content: Content
public let axes: Axis.Set
public let showsIndicators: Bool

View File

@ -25,7 +25,7 @@ public enum HorizontalAlignment: Equatable {
/// Text("Hello")
/// Text("World")
/// }
public struct VStack<Content>: PrimitiveView where Content: View {
public struct VStack<Content>: _PrimitiveView where Content: View {
public let alignment: HorizontalAlignment
public let spacing: CGFloat?
public let content: Content

View File

@ -43,7 +43,7 @@ public struct Alignment: Equatable {
/// Text("Top")
/// }
///
public struct ZStack<Content>: PrimitiveView where Content: View {
public struct ZStack<Content>: _PrimitiveView where Content: View {
public let alignment: Alignment
public let spacing: CGFloat?
public let content: Content

View File

@ -22,7 +22,7 @@ final class NavigationLinkDestination {
}
}
public struct NavigationLink<Label, Destination>: PrimitiveView where Label: View,
public struct NavigationLink<Label, Destination>: _PrimitiveView where Label: View,
Destination: View
{
@State var destination: NavigationLinkDestination

View File

@ -19,7 +19,7 @@ public final class NavigationContext: ObservableObject {
@Published var destination = NavigationLinkDestination(EmptyView())
}
public struct NavigationView<Content>: PrimitiveView where Content: View {
public struct NavigationView<Content>: _PrimitiveView where Content: View {
let content: Content
@StateObject var context = NavigationContext()

View File

@ -20,7 +20,7 @@ import struct Foundation.Date
/// A control for selecting an absolute date.
///
/// Available when `Label` conform to `View`.
public struct DatePicker<Label>: PrimitiveView where Label: View {
public struct DatePicker<Label>: _PrimitiveView where Label: View {
let label: Label
let valueBinding: Binding<Date>
let displayedComponents: DatePickerComponents

View File

@ -16,7 +16,11 @@ public protocol _PickerContainerProtocol {
var elements: [_AnyIDView] { get }
}
public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: View>: PrimitiveView,
public struct _PickerContainer<
Label: View,
SelectionValue: Hashable,
Content: View
>: _PrimitiveView,
_PickerContainerProtocol
{
@Binding public var selection: SelectionValue
@ -38,7 +42,7 @@ public struct _PickerContainer<Label: View, SelectionValue: Hashable, Content: V
}
}
public struct _PickerElement: PrimitiveView {
public struct _PickerElement: _PrimitiveView {
public let valueIndex: Int?
public let content: AnyView
@Environment(\.pickerStyle) public var style

View File

@ -28,7 +28,7 @@ private func convert<T: BinaryFloatingPoint>(_ range: ClosedRange<T>) -> ClosedR
/// A control for selecting a value from a bounded linear range of values.
///
/// Available when `Label` and `ValueLabel` conform to `View`.
public struct Slider<Label, ValueLabel>: PrimitiveView where Label: View, ValueLabel: View {
public struct Slider<Label, ValueLabel>: _PrimitiveView where Label: View, ValueLabel: View {
let label: Label
let minValueLabel: ValueLabel
let maxValueLabel: ValueLabel

View File

@ -16,7 +16,7 @@
//
/// A horizontal line for separating content.
public struct Divider: PrimitiveView {
public struct Divider: _PrimitiveView {
@Environment(\.self) public var environment
public init() {}

View File

@ -22,7 +22,7 @@
/// Spacer()
/// Text("World")
/// }
public struct Spacer: PrimitiveView {
public struct Spacer: _PrimitiveView {
public var minLength: CGFloat?
public init(minLength: CGFloat? = nil) {

View File

@ -35,7 +35,7 @@
/// print("Set password")
/// })
/// }
public struct SecureField<Label>: PrimitiveView where Label: View {
public struct SecureField<Label>: _PrimitiveView where Label: View {
let label: Label
let textBinding: Binding<String>
let onCommit: () -> ()

View File

@ -29,7 +29,7 @@
/// .bold()
/// .italic()
/// .underline(true, color: .red)
public struct Text: PrimitiveView {
public struct Text: _PrimitiveView {
let storage: _Storage
let modifiers: [_Modifier]

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
public struct TextEditor: PrimitiveView {
public struct TextEditor: _PrimitiveView {
let textBinding: Binding<String>
public init(text: Binding<String>) {

View File

@ -34,7 +34,7 @@
/// print("Set username")
/// })
/// }
public struct TextField<Label>: PrimitiveView where Label: View {
public struct TextField<Label>: _PrimitiveView where Label: View {
let label: Label
let textBinding: Binding<String>
let onEditingChanged: (Bool) -> ()

View File

@ -28,12 +28,12 @@ public extension Never {
}
}
extension Never: PrimitiveView {}
extension Never: _PrimitiveView {}
/// A `View` that offers primitive functionality, which renders its `body` inaccessible.
public protocol PrimitiveView: View where Body == Never {}
public protocol _PrimitiveView: View where Body == Never {}
public extension PrimitiveView {
public extension _PrimitiveView {
@_spi(TokamakCore)
var body: Never {
neverBody(String(reflecting: Self.self))
@ -48,15 +48,6 @@ public protocol ParentView {
/// A `View` type that is not rendered but "flattened", rendering all its children instead.
protocol GroupView: ParentView {}
/** The distinction between "host" (truly primitive) and "composite" (that have meaningful `body`)
views is made in the reconciler in `TokamakCore` based on their `body` type, host views have body
type `Never`. `ViewDeferredToRenderer` allows renderers to override that per-platform and render
host views as composite by providing their own `deferredBody` implementation.
*/
public protocol ViewDeferredToRenderer {
var deferredBody: AnyView { get }
}
/// Calls `fatalError` with an explanation that a given `type` is a primitive `View`
public func neverBody(_ type: String) -> Never {
fatalError("\(type) is a primitive `View`, you're not supposed to access its `body`.")

View File

@ -16,13 +16,13 @@
//
/// A `View` with no effect on rendering.
public struct EmptyView: PrimitiveView {
public struct EmptyView: _PrimitiveView {
@inlinable
public init() {}
}
// swiftlint:disable:next type_name
public struct _ConditionalContent<TrueContent, FalseContent>: PrimitiveView
public struct _ConditionalContent<TrueContent, FalseContent>: _PrimitiveView
where TrueContent: View, FalseContent: View
{
enum Storage {

View File

@ -26,6 +26,7 @@ enum ColorSchemeObserver {
static func observe(_ rootElement: JSObject) {
let closure = JSClosure {
publisher.value = .init(matchMediaDarkScheme: $0[0].object!)
return .undefined
}
_ = matchMediaDarkScheme.addListener!(closure)
Self.closure = closure

View File

@ -21,13 +21,14 @@ enum ScenePhaseObserver {
private static var closure: JSClosure?
static func observe() {
let closure = JSClosure { _ -> () in
let closure = JSClosure { _ -> JSValue in
let visibilityState = document.visibilityState.string
if visibilityState == "visible" {
publisher.send(.active)
} else if visibilityState == "hidden" {
publisher.send(.background)
}
return .undefined
}
_ = document.addEventListener!("visibilitychange", closure)
Self.closure = closure

View File

@ -84,6 +84,7 @@ final class DOMNode: Target {
for (event, listener) in listeners {
let jsClosure = JSClosure {
listener($0[0].object!)
return .undefined
}
_ = ref.addEventListener!(event, jsClosure)
self.listeners[event] = jsClosure

View File

@ -171,4 +171,16 @@ final class DOMRenderer: Renderer {
_ = try? parent.ref.throwing.removeChild!(target.ref)
}
func primitiveBody(for view: Any) -> AnyView? {
(view as? DOMPrimitive)?.renderedBody ?? (view as? _HTMLPrimitive)?.renderedBody
}
func isPrimitiveView(_ type: Any.Type) -> Bool {
type is DOMPrimitive.Type || type is _HTMLPrimitive.Type
}
}
protocol DOMPrimitive {
var renderedBody: AnyView { get }
}

View File

@ -25,6 +25,7 @@ private let localStorage = JSObject.global.localStorage.object!
public class LocalStorage: WebStorage, _StorageProvider {
static let closure = JSClosure { _ in
rootPublisher.send()
return .undefined
}
let storage = localStorage

View File

@ -17,9 +17,8 @@
import TokamakCore
extension ButtonStyleConfiguration.Label: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension ButtonStyleConfiguration.Label: DOMPrimitive {
var renderedBody: AnyView {
_ButtonStyleConfigurationProxy.Label(self).content
}
}

View File

@ -18,9 +18,9 @@
import TokamakCore
import TokamakStaticHTML
extension _Button: ViewDeferredToRenderer {
extension _Button: DOMPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
var attributes: [HTMLAttribute: String] = [:]
let listeners: [String: Listener] = [
"pointerdown": { _ in isPressed = true },

View File

@ -18,7 +18,7 @@
import TokamakCore
import TokamakStaticHTML
extension DisclosureGroup: ViewDeferredToRenderer {
extension DisclosureGroup: DOMPrimitive {
var chevron: some View {
DynamicHTML(
"div",
@ -70,8 +70,7 @@ extension DisclosureGroup: ViewDeferredToRenderer {
}
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
var renderedBody: AnyView {
AnyView(HTML("div", [
"class": "_tokamak-disclosuregroup",
"role": "tree",

View File

@ -35,9 +35,9 @@ public struct DynamicHTML<Content>: View, AnyDynamicHTML {
fileprivate let cachedInnerHTML: String?
public func innerHTML(shouldSortAttributes: Bool) -> String? {
cachedInnerHTML
}
public func innerHTML(shouldSortAttributes: Bool) -> String? {
cachedInnerHTML
}
@_spi(TokamakCore)
public var body: Never {

View File

@ -18,9 +18,8 @@ import TokamakStaticHTML
private let ResizeObserver = JSObject.global.ResizeObserver.function!
extension GeometryReader: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension GeometryReader: DOMPrimitive {
var renderedBody: AnyView {
AnyView(_GeometryReader(content: content))
}
}
@ -56,16 +55,17 @@ struct _GeometryReader<Content: View>: View {
}
._domRef($state.observedNodeRef)
._onMount {
let closure = JSClosure { [weak state] args -> () in
let closure = JSClosure { [weak state] args -> JSValue in
// FIXME: `JSArrayRef` is not a `RandomAccessCollection` for some reason, which forces
// us to use a string subscript
guard
let rect = args[0].object?[dynamicMember: "0"].object?.contentRect.object,
let width = rect.width.number,
let height = rect.height.number
else { return }
else { return .undefined }
state?.size = .init(width: width, height: height)
return .undefined
}
state.closure = closure

View File

@ -15,9 +15,8 @@
import TokamakCore
import TokamakStaticHTML
extension NavigationLink: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension NavigationLink: DOMPrimitive {
var renderedBody: AnyView {
let proxy = _NavigationLinkProxy(self)
return AnyView(
DynamicHTML("a", [

View File

@ -20,9 +20,8 @@ import JavaScriptKit
import TokamakCore
import TokamakStaticHTML
extension DatePicker: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension DatePicker: DOMPrimitive {
var renderedBody: AnyView {
let proxy = _DatePickerProxy(self)
let type = proxy.displayedComponents

View File

@ -16,9 +16,8 @@ import JavaScriptKit
import TokamakCore
import TokamakStaticHTML
extension _PickerContainer: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension _PickerContainer: DOMPrimitive {
var renderedBody: AnyView {
AnyView(HTML("label") {
label
Text(" ")
@ -35,9 +34,8 @@ extension _PickerContainer: ViewDeferredToRenderer {
}
}
extension _PickerElement: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension _PickerElement: DOMPrimitive {
var renderedBody: AnyView {
let attributes: [HTMLAttribute: String]
if let value = valueIndex {
attributes = [.value: "\(value)"]

View File

@ -16,9 +16,8 @@ import JavaScriptKit
import TokamakCore
import TokamakStaticHTML
extension Slider: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension Slider: DOMPrimitive {
var renderedBody: AnyView {
let proxy = _SliderProxy(self)
let step: String
switch proxy.step {

View File

@ -17,9 +17,8 @@
import TokamakCore
extension SecureField: ViewDeferredToRenderer where Label == Text {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension SecureField: DOMPrimitive where Label == Text {
var renderedBody: AnyView {
let proxy = _SecureFieldProxy(self)
return AnyView(DynamicHTML("input", [
"type": "password",

View File

@ -14,9 +14,8 @@
import TokamakCore
extension TextEditor: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension TextEditor: DOMPrimitive {
var renderedBody: AnyView {
let proxy = _TextEditorProxy(self)
return AnyView(DynamicHTML("textarea", [

View File

@ -18,7 +18,7 @@
import TokamakCore
import TokamakStaticHTML
extension TextField: ViewDeferredToRenderer where Label == Text {
extension TextField: DOMPrimitive where Label == Text {
func css(for style: TextFieldStyle) -> String {
if style is PlainTextFieldStyle {
return """
@ -39,8 +39,7 @@ extension TextField: ViewDeferredToRenderer where Label == Text {
}
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
var renderedBody: AnyView {
let proxy = _TextFieldProxy(self)
return AnyView(DynamicHTML("input", [

View File

@ -27,9 +27,10 @@ private final class HashState: ObservableObject {
init() {
let onHashChange = JSClosure { [weak self] _ in
self?.currentHash = location.hash.string!
return .undefined
}
window.onhashchange = .function(onHashChange)
window.onhashchange = .object(onHashChange)
self.onHashChange = onHashChange
}

View File

@ -133,4 +133,16 @@ final class GTKRenderer: Renderer {
target.destroy()
}
public func isPrimitiveView(_ type: Any.Type) -> Bool {
type is GTKPrimitive.Type
}
public func primitiveBody(for view: Any) -> AnyView? {
(view as? GTKPrimitive)?.renderedBody
}
}
protocol GTKPrimitive {
var renderedBody: AnyView { get }
}

View File

@ -50,16 +50,16 @@ extension WidgetAttributeModifier {
}
}
extension ModifiedContent: ViewDeferredToRenderer where Content: View {
extension ModifiedContent: GTKPrimitive where Content: View {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
guard let widgetModifier = modifier as? WidgetModifier else {
return AnyView(content)
}
let anyWidget: AnyWidget
if let anyView = content as? ViewDeferredToRenderer,
if let anyView = content as? GTKPrimitive,
let _anyWidget = mapAnyView(
anyView.deferredBody,
anyView.renderedBody,
transform: { (widget: AnyWidget) in widget }
)
{

View File

@ -50,9 +50,9 @@ func createPath(from elements: [Path.Element], in cr: OpaquePointer) {
}
}
extension _ShapeView: ViewDeferredToRenderer {
extension _ShapeView: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
AnyView(WidgetView(build: { _ in
let w = gtk_drawing_area_new()
bindAction(to: w!)

View File

@ -15,7 +15,7 @@
import CGTK
import TokamakCore
extension List: ViewDeferredToRenderer {
extension List: GTKPrimitive {
@ViewBuilder
func iterateAsRow(_ content: [AnyView]) -> some View {
ForEach(Array(content.enumerated()), id: \.offset) { _, row in
@ -32,7 +32,7 @@ extension List: ViewDeferredToRenderer {
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
let proxy = _ListProxy(self)
return AnyView(ScrollView {
WidgetView(build: { _ in

View File

@ -58,9 +58,9 @@ protocol GtkStackProtocol {}
// }
// }
extension NavigationView: ViewDeferredToRenderer {
extension NavigationView: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
let proxy = _NavigationViewProxy(self)
return AnyView(HStack {
proxy.content
@ -70,9 +70,9 @@ extension NavigationView: ViewDeferredToRenderer {
}
}
extension NavigationLink: ViewDeferredToRenderer {
extension NavigationLink: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
let proxy = _NavigationLinkProxy(self)
return AnyView(Button(action: { proxy.activate() }) {
proxy.label
@ -137,8 +137,8 @@ extension NavigationLink: ViewDeferredToRenderer {
// [AnyView(_NavigationLinkProxy(self).label)]
// }
// }
// extension NavigationLink: ViewDeferredToRenderer {
// public var deferredBody: AnyView {
// extension NavigationLink: GTKPrimitive {
// public var renderedBody: AnyView {
// let proxy = _NavigationLinkProxy(self)
// print("Selected: \(proxy.isSelected)")
// return AnyView(Button {

View File

@ -18,9 +18,9 @@
import CGTK
import TokamakCore
extension ScrollView: ViewDeferredToRenderer {
extension ScrollView: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
AnyView(WidgetView(build: { _ in
gtk_scrolled_window_new(nil, nil)
}) {

View File

@ -52,9 +52,9 @@ struct Box<Content: View>: View, ParentView, AnyWidget, StackProtocol {
}
}
extension VStack: ViewDeferredToRenderer {
extension VStack: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
AnyView(
Box(
content: content,
@ -66,9 +66,9 @@ extension VStack: ViewDeferredToRenderer {
}
}
extension HStack: ViewDeferredToRenderer {
extension HStack: GTKPrimitive {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
AnyView(
Box(
content: content,

View File

@ -56,9 +56,9 @@ private func bindAction(to entry: UnsafeMutablePointer<GtkWidget>, textBinding:
})
}
extension SecureField: ViewDeferredToRenderer where Label == Text {
extension SecureField: GTKPrimitive where Label == Text {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
let proxy = _SecureFieldProxy(self)
return AnyView(WidgetView(
build: { _ in
@ -72,9 +72,9 @@ extension SecureField: ViewDeferredToRenderer where Label == Text {
}
}
extension TextField: ViewDeferredToRenderer where Label == Text {
extension TextField: GTKPrimitive where Label == Text {
@_spi(TokamakCore)
public var deferredBody: AnyView {
public var renderedBody: AnyView {
let proxy = _TextFieldProxy(self)
return AnyView(WidgetView(
build: { _ in

View File

@ -29,9 +29,9 @@ extension ModifiedContent: AnyModifiedContent where Modifier: DOMViewModifier, C
}
}
extension ModifiedContent: ViewDeferredToRenderer where Content: View, Modifier: ViewModifier {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension ModifiedContent: _HTMLPrimitive where Content: View, Modifier: ViewModifier {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
if let domModifier = modifier as? DOMViewModifier {
if let adjacentModifier = content as? AnyModifiedContent,
!(adjacentModifier.anyModifier.isOrderDependent || domModifier.isOrderDependent)

View File

@ -22,7 +22,7 @@ extension StrokeStyle {
}
}
extension Path: ViewDeferredToRenderer {
extension Path: _HTMLPrimitive {
// TODO: Support transformations
func svgFrom(
storage: Storage,
@ -128,8 +128,8 @@ extension Path: ViewDeferredToRenderer {
svgFrom(storage: storage, strokeStyle: strokeStyle)
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
let sizeStyle = sizing == .flexible ?
"""
width: 100%;

View File

@ -31,10 +31,10 @@ extension _StrokedShape: ShapeAttributes {
}
}
extension _ShapeView: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
let path = shape.path(in: .zero).deferredBody
extension _ShapeView: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
let path = shape.path(in: .zero).renderedBody
if let shapeAttributes = shape as? ShapeAttributes {
return AnyView(HTML("div", shapeAttributes.attributes(style)) { path })
} else if let color = style as? Color {

View File

@ -141,4 +141,16 @@ public final class StaticHTMLRenderer: Renderer {
) {
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
public func isPrimitiveView(_ type: Any.Type) -> Bool {
type is _HTMLPrimitive.Type
}
public func primitiveBody(for view: Any) -> AnyView? {
(view as? _HTMLPrimitive)?.renderedBody
}
}
public protocol _HTMLPrimitive {
var renderedBody: AnyView { get }
}

View File

@ -17,9 +17,9 @@
import TokamakCore
extension Link: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension Link: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
let proxy = _LinkProxy(self)
return AnyView(HTML("a", ["href": proxy.destination.absoluteString, "class": "_tokamak-link"]) {
proxy.label

View File

@ -47,30 +47,31 @@ extension HTMLAttribute: ExpressibleByStringLiteral {
}
public protocol AnyHTML {
func innerHTML(shouldSortAttributes: Bool) -> String?
func innerHTML(shouldSortAttributes: Bool) -> String?
var tag: String { get }
var attributes: [HTMLAttribute: String] { get }
}
public extension AnyHTML {
func outerHTML(shouldSortAttributes: Bool, children: [HTMLTarget]) -> String {
let renderedAttributes: String
if attributes.isEmpty {
renderedAttributes = ""
} else {
let mappedAttributes = attributes.map { #"\#($0)="\#($1)""# }
if shouldSortAttributes {
renderedAttributes = mappedAttributes.sorted().joined(separator: " ")
} else {
renderedAttributes = mappedAttributes.joined(separator: " ")
}
}
func outerHTML(shouldSortAttributes: Bool, children: [HTMLTarget]) -> String {
let renderedAttributes: String
if attributes.isEmpty {
renderedAttributes = ""
} else {
let mappedAttributes = attributes.map { #"\#($0)="\#($1)""# }
if shouldSortAttributes {
renderedAttributes = mappedAttributes.sorted().joined(separator: " ")
} else {
renderedAttributes = mappedAttributes.joined(separator: " ")
}
}
return """
<\(tag)\(attributes.isEmpty ? "" : " ")\
\(renderedAttributes)>\
\(innerHTML(shouldSortAttributes: shouldSortAttributes) ?? "")\
\(children.map { $0.outerHTML(shouldSortAttributes: shouldSortAttributes) }.joined(separator: "\n"))\
\(children.map { $0.outerHTML(shouldSortAttributes: shouldSortAttributes) }
.joined(separator: "\n"))\
</\(tag)>
"""
}
@ -81,12 +82,11 @@ public struct HTML<Content>: View, AnyHTML {
public let attributes: [HTMLAttribute: String]
let content: Content
fileprivate let cachedInnerHTML: String?
public func innerHTML(shouldSortAttributes: Bool) -> String? {
cachedInnerHTML
}
fileprivate let cachedInnerHTML: String?
public func innerHTML(shouldSortAttributes: Bool) -> String? {
cachedInnerHTML
}
@_spi(TokamakCore)
public var body: Never {

View File

@ -19,9 +19,9 @@ import TokamakCore
public typealias Image = TokamakCore.Image
extension Image: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension Image: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
AnyView(_HTMLImage(proxy: _ImageProxy(self)))
}
}

View File

@ -27,11 +27,11 @@ extension VerticalAlignment {
}
}
extension HStack: ViewDeferredToRenderer, SpacerContainer {
extension HStack: _HTMLPrimitive, SpacerContainer {
public var axis: SpacerContainerAxis { .horizontal }
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
AnyView(HTML("div", [
"style": """
display: flex; flex-direction: row; align-items: \(alignment.cssValue);

View File

@ -31,13 +31,13 @@ extension LazyHGrid: SpacerContainer {
}
}
extension LazyHGrid: ViewDeferredToRenderer {
extension LazyHGrid: _HTMLPrimitive {
public var lastRow: GridItem? {
_LazyHGridProxy(self).rows.last
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
var styles = """
display: grid;
grid-template-rows: \(_LazyHGridProxy(self)

View File

@ -31,13 +31,13 @@ extension LazyVGrid: SpacerContainer {
}
}
extension LazyVGrid: ViewDeferredToRenderer {
public var lastColumn: GridItem? {
extension LazyVGrid: _HTMLPrimitive {
var lastColumn: GridItem? {
_LazyVGridProxy(self).columns.last
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
var styles = """
display: grid;
grid-template-columns: \(_LazyVGridProxy(self)

View File

@ -17,7 +17,7 @@
import TokamakCore
extension ScrollView: ViewDeferredToRenderer, SpacerContainer {
extension ScrollView: _HTMLPrimitive, SpacerContainer {
public var axis: SpacerContainerAxis {
if axes.contains(.horizontal) {
return .horizontal
@ -26,8 +26,8 @@ extension ScrollView: ViewDeferredToRenderer, SpacerContainer {
}
}
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
let scrollX = axes.contains(.horizontal)
let scrollY = axes.contains(.vertical)
return AnyView(HTML("div", [

View File

@ -27,11 +27,11 @@ extension HorizontalAlignment {
}
}
extension VStack: ViewDeferredToRenderer, SpacerContainer {
extension VStack: _HTMLPrimitive, SpacerContainer {
public var axis: SpacerContainerAxis { .vertical }
@_spi(TokamakCore)
public var deferredBody: AnyView {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
AnyView(HTML("div", [
"style": """
display: flex; flex-direction: column; align-items: \(alignment.cssValue);

View File

@ -22,9 +22,9 @@ struct _ZStack_ContentGridItem: ViewModifier, DOMViewModifier {
}
}
extension ZStack: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension ZStack: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
AnyView(HTML("div", [
"style": """
display: grid;

View File

@ -14,9 +14,9 @@
import TokamakCore
extension NavigationView: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension NavigationView: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
let proxy = _NavigationViewProxy(self)
return AnyView(HTML("div", [
"class": "_tokamak-navigationview",

View File

@ -15,7 +15,7 @@
@_spi(TokamakCore) import TokamakCore
extension Divider: AnyHTML {
public func innerHTML(shouldSortAttributes: Bool) -> String? { nil }
public func innerHTML(shouldSortAttributes: Bool) -> String? { nil }
public var tag: String { "hr" }
public var attributes: [HTMLAttribute: String] {
[

View File

@ -55,9 +55,9 @@ public extension SpacerContainer where Self: ParentView {
}
}
extension Spacer: ViewDeferredToRenderer {
@_spi(TokamakCore)
public var deferredBody: AnyView {
extension Spacer: _HTMLPrimitive {
@_spi(TokamakStaticHTML)
public var renderedBody: AnyView {
AnyView(HTML("div", [
"style": "flex-grow: 1; \(minLength != nil ? "min-width: \(minLength!)" : "")",
]))

View File

@ -105,12 +105,12 @@ private struct TextSpan: AnyHTML {
let content: String
let attributes: [HTMLAttribute: String]
public func innerHTML(shouldSortAttributes: Bool) -> String? { content }
public func innerHTML(shouldSortAttributes: Bool) -> String? { content }
var tag: String { "span" }
}
extension Text: AnyHTML {
public func innerHTML(shouldSortAttributes: Bool) -> String? {
public func innerHTML(shouldSortAttributes: Bool) -> String? {
let proxy = _TextProxy(self)
let innerHTML: String
switch proxy.storage {

View File

@ -28,19 +28,19 @@ struct BenchmarkApp: App {
}
benchmark("render App unsorted attributes") {
_ = StaticHTMLRenderer(BenchmarkApp()).render(shouldSortAttributes: false)
_ = StaticHTMLRenderer(BenchmarkApp()).render(shouldSortAttributes: false)
}
benchmark("render App sorted attributes") {
_ = StaticHTMLRenderer(BenchmarkApp()).render(shouldSortAttributes: true)
_ = StaticHTMLRenderer(BenchmarkApp()).render(shouldSortAttributes: true)
}
benchmark("render List unsorted attributes") {
_ = StaticHTMLRenderer(List(1..<100) { Text("\($0)") }).render(shouldSortAttributes: false)
_ = StaticHTMLRenderer(List(1..<100) { Text("\($0)") }).render(shouldSortAttributes: false)
}
benchmark("render List sorted attributes") {
_ = StaticHTMLRenderer(List(1..<100) { Text("\($0)") }).render(shouldSortAttributes: true)
_ = StaticHTMLRenderer(List(1..<100) { Text("\($0)") }).render(shouldSortAttributes: true)
}
Benchmark.main()

View File

@ -63,4 +63,12 @@ public final class TestRenderer: Renderer {
) {
target.removeFromSuperview()
}
public func primitiveBody(for view: Any) -> AnyView? {
nil
}
public func isPrimitiveView(_ type: Any.Type) -> Bool {
false
}
}

View File

@ -18,6 +18,149 @@
import TokamakStaticHTML
import XCTest
private let expectedHTML =
#"""
<html>
<head>
<title></title>
<style>
._tokamak-stack > * {
flex-shrink: 0;
}
._tokamak-scrollview-hideindicators {
scrollbar-color: transparent;
scrollbar-width: 0;
}
._tokamak-scrollview-hideindicators::-webkit-scrollbar {
width: 0;
height: 0;
}
._tokamak-list {
list-style: none;
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0;
}
._tokamak-disclosuregroup-label {
cursor: pointer;
}
._tokamak-disclosuregroup-chevron-container {
width: .25em;
height: .25em;
padding: 10px;
display: inline-block;
}
._tokamak-disclosuregroup-chevron {
width: 100%;
height: 100%;
transform: rotate(45deg);
border-right: solid 2px rgba(0, 0, 0, 0.25);
border-top: solid 2px rgba(0, 0, 0, 0.25);
}
._tokamak-disclosuregroup-content {
display: flex;
flex-direction: column;
margin-left: 1em;
}
._tokamak-buttonstyle-reset {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: transparent;
border: none;
margin: 0;
padding: 0;
font-size: inherit;
}
._tokamak-text-redacted {
position: relative;
}
._tokamak-text-redacted::after {
content: " ";
background-color: rgb(200, 200, 200);
position: absolute;
left: 0;
width: calc(100% + .1em);
height: 1.2em;
border-radius: .1em;
}
._tokamak-geometryreader {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
._tokamak-navigationview {
display: flex;
flex-direction: row;
align-items: stretch;
width: 100%;
height: 100%;
}
._tokamak-navigationview-content {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
flex-grow: 1;
height: 100%;
}
._tokamak-formcontrol {
color-scheme: light dark;
}
._tokamak-link {
text-decoration: none;
}
._tokamak-texteditor {
width: 100%;
height: 100%;
}
@media (prefers-color-scheme:dark) {
._tokamak-text-redacted::after {
background-color: rgb(100, 100, 100);
}
._tokamak-disclosuregroup-chevron {
border-right-color: rgba(255, 255, 255, 0.25);
border-top-color: rgba(255, 255, 255, 0.25);
}
}
</style>
</head>
<body style="margin: 0;display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
overflow: hidden;"><div class="_tokamak-stack" style="display: flex; flex-direction: column; align-items: center;
height: 100%;
"><span class="" style="
font-family: system,
-apple-system,
'.SFNSText-Regular',
'San Francisco',
'Roboto',
'Segoe UI',
'Helvetica Neue',
'Lucida Grande',
sans-serif;
color: rgba(0.0, 0.0, 0.0, 1.0);
font-style: normal;
font-weight: 400;
letter-spacing: normal;
vertical-align: baseline;
text-decoration: none;
text-decoration-color: inherit;
text-align: left;">text</span>
<div style="flex-grow: 1; "></div></div></body>
</html>
"""#
final class ReconcilerTests: XCTestCase {
struct Model {
let text: Text
@ -38,8 +181,9 @@ final class ReconcilerTests: XCTestCase {
}
func testOptional() {
let renderer = StaticHTMLRenderer(OptionalBody(model: Model(text: Text("text"))))
let resultingHTML = StaticHTMLRenderer(OptionalBody(model: Model(text: Text("text"))))
.render(shouldSortAttributes: true)
XCTAssertEqual(renderer.html.count, 2777)
XCTAssertEqual(resultingHTML, expectedHTML)
}
}

View File

@ -305,7 +305,51 @@ bring Tokamak to.
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
This is done via a few additional functions in the `Renderer` protocol.
```swift
public protocol Renderer: AnyObject {
// ...
// Functions unrelated to this feature skipped for brevity.
/** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
this renderer.
*/
func primitiveBody(for view: Any) -> AnyView?
/** Returns `true` if a given view type is a primitive view that should be deferred to this
renderer.
*/
func isPrimitiveView(_ type: Any.Type) -> Bool
}
```
This allows to declare a renderer-specific protocol for these views. Let's call it `HTMLPrimitive`:
```swift
public protocol HTMLPrimitive {
var renderedBody: AnyView { get }
}
```
Then add the implementation using this protocol to your `StaticHTMLRenderer`:
```swift
public final class StaticHTMLRenderer: Renderer {
// ...
// Rest of the functions skipped for brevity.
public func isPrimitiveView(_ type: Any.Type) -> Bool {
type is HTMLPrimitive.Type
}
public func primitiveBody(for view: Any) -> AnyView? {
(view as? HTMLPrimitive)?.renderedBody
}
}
```
In a conformance to `HTMLPrimitive` we can provide a `View` that our
`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the
`HTML` view. Lets look at a simpler version of this view:
@ -330,13 +374,13 @@ Here we define an `HTML` view to have a body type of `Never`, like other primiti
conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without
worrying about the `associatedtypes` involved with `View`.
### `ViewDeferredToRenderer`
### `HTMLPrimitive`
Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`:
```swift
extension Text: ViewDeferredToRenderer {
var deferredBody: AnyView {
extension Text: HTMLPrimitive {
var renderedBody: AnyView {
AnyView(HTML("span", [:], _TextProxy(self).rawText))
}
}