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:
parent
da9843d07f
commit
5926e9f182
|
@ -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"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
public enum TextAlignment: Hashable, CaseIterable {
|
||||
case leading,
|
||||
center,
|
||||
trailing
|
||||
center,
|
||||
trailing
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>?
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct Image: PrimitiveView {
|
||||
public struct Image: _PrimitiveView {
|
||||
let label: Text?
|
||||
let name: String
|
||||
let bundle: Bundle?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
/// Spacer()
|
||||
/// Text("World")
|
||||
/// }
|
||||
public struct Spacer: PrimitiveView {
|
||||
public struct Spacer: _PrimitiveView {
|
||||
public var minLength: CGFloat?
|
||||
|
||||
public init(minLength: CGFloat? = nil) {
|
||||
|
|
|
@ -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: () -> ()
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
/// .bold()
|
||||
/// .italic()
|
||||
/// .underline(true, color: .red)
|
||||
public struct Text: PrimitiveView {
|
||||
public struct Text: _PrimitiveView {
|
||||
let storage: _Storage
|
||||
let modifiers: [_Modifier]
|
||||
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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) -> ()
|
||||
|
|
|
@ -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`.")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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", [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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", [
|
||||
|
|
|
@ -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", [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
{
|
||||
|
|
|
@ -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!)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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] {
|
||||
[
|
||||
|
|
|
@ -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!)" : "")",
|
||||
]))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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. Let’s 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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue