Merge b1d358aabb
into e0d8e9db46
This commit is contained in:
commit
a59b9c6c67
|
@ -15,6 +15,11 @@
|
|||
// Created by Carson Katri on 7/16/20.
|
||||
//
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public struct _WindowGroupTitle: _PrimitiveView {
|
||||
public let title: Text?
|
||||
}
|
||||
|
||||
public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
||||
public let id: String
|
||||
public let title: Text?
|
||||
|
@ -77,6 +82,9 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
// }
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
|
||||
visitor.visit(content)
|
||||
visitor.visit(Group {
|
||||
_WindowGroupTitle(title: self.title)
|
||||
content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ extension FiberReconciler.Fiber: CustomDebugStringConvertible {
|
|||
private func flush(level: Int = 0) -> String {
|
||||
let spaces = String(repeating: " ", count: level)
|
||||
let geometry = geometry ?? .init(
|
||||
origin: .init(origin: .zero),
|
||||
origin: .init(parent: .zero, origin: .zero),
|
||||
dimensions: .init(size: .zero, alignmentGuides: [:]),
|
||||
proposal: .unspecified
|
||||
)
|
||||
|
|
|
@ -423,7 +423,8 @@ public extension FiberReconciler {
|
|||
environment: .init(rootEnvironment),
|
||||
traits: .init(),
|
||||
preferenceStore: preferences
|
||||
)
|
||||
),
|
||||
preferenceStore: preferences ?? .init()
|
||||
)
|
||||
if let preferenceStore = outputs.preferenceStore {
|
||||
preferences = preferenceStore
|
||||
|
|
|
@ -41,7 +41,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
|
||||
private var sceneSizeCancellable: AnyCancellable?
|
||||
|
||||
private var isReconciling = false
|
||||
/// The identifiers for each `Fiber` that changed state during the last run loop.
|
||||
///
|
||||
/// The reconciler loop starts at the root of the `View` hierarchy
|
||||
|
@ -49,7 +48,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
/// To help mitigate performance issues related to this, we only perform reconcile
|
||||
/// checks when we reach a changed `Fiber`.
|
||||
private var changedFibers = Set<ObjectIdentifier>()
|
||||
public var afterReconcileActions = [() -> ()]()
|
||||
|
||||
struct RootView<Content: View>: View {
|
||||
let content: Content
|
||||
|
@ -59,7 +57,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
var environment = reconciler.renderer.defaultEnvironment
|
||||
environment.measureText = reconciler.renderer.measureText
|
||||
environment.measureImage = reconciler.renderer.measureImage
|
||||
environment.afterReconcile = reconciler.afterReconcile
|
||||
return environment
|
||||
}
|
||||
|
||||
|
@ -69,6 +66,10 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
.environmentValues(environment)
|
||||
}
|
||||
}
|
||||
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
.init(inputs: inputs, preferenceStore: inputs.preferenceStore ?? .init())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Layout` container for the root of a `View` hierarchy.
|
||||
|
@ -140,7 +141,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
var environment = renderer.defaultEnvironment
|
||||
environment.measureText = renderer.measureText
|
||||
environment.measureImage = renderer.measureImage
|
||||
environment.afterReconcile = afterReconcile
|
||||
var app = app
|
||||
current = .init(
|
||||
&app,
|
||||
|
@ -215,15 +215,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
}
|
||||
}
|
||||
|
||||
func afterReconcile(_ action: @escaping () -> ()) {
|
||||
guard isReconciling == true
|
||||
else {
|
||||
action()
|
||||
return
|
||||
}
|
||||
afterReconcileActions.append(action)
|
||||
}
|
||||
|
||||
/// Called by any `Fiber` that experiences a state change.
|
||||
///
|
||||
/// Reconciliation only runs after every change during the current run loop has been performed.
|
||||
|
@ -243,7 +234,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
///
|
||||
/// A `reconcile()` call is queued from `fiberChanged` once per run loop.
|
||||
func reconcile() {
|
||||
isReconciling = true
|
||||
let changedFibers = changedFibers
|
||||
self.changedFibers.removeAll()
|
||||
// Create a list of mutations.
|
||||
|
@ -270,21 +260,8 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
|||
self.alternate = current
|
||||
current = alternate
|
||||
|
||||
isReconciling = false
|
||||
|
||||
for action in afterReconcileActions {
|
||||
action()
|
||||
if let preferences = current.preferences {
|
||||
renderer.preferencesChanged(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
private enum AfterReconcileKey: EnvironmentKey {
|
||||
static let defaultValue: (@escaping () -> ()) -> () = { _ in }
|
||||
}
|
||||
|
||||
var afterReconcile: (@escaping () -> ()) -> () {
|
||||
get { self[AfterReconcileKey.self] }
|
||||
set { self[AfterReconcileKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,9 @@ public protocol FiberRenderer {
|
|||
/// (in this case just `DuelOfTheStates` as both properties were on it),
|
||||
/// and reconcile after all changes have been collected.
|
||||
func schedule(_ action: @escaping () -> ())
|
||||
|
||||
/// Called by the reconciler when the preferences of the topmost `Fiber` changed.
|
||||
func preferencesChanged(_ preferenceStore: _PreferenceStore)
|
||||
}
|
||||
|
||||
public extension FiberRenderer {
|
||||
|
@ -107,6 +110,8 @@ public extension FiberRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
func preferencesChanged(_ preferenceStore: _PreferenceStore) {}
|
||||
|
||||
@discardableResult
|
||||
@_disfavoredOverload
|
||||
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
|
||||
|
|
|
@ -22,15 +22,20 @@ public struct LayoutSubviews: Equatable, RandomAccessCollection {
|
|||
public var layoutDirection: LayoutDirection
|
||||
var storage: [LayoutSubview]
|
||||
|
||||
init(layoutDirection: LayoutDirection, storage: [LayoutSubview]) {
|
||||
@_spi(TokamakCore)
|
||||
public var globalOrigin: CGPoint
|
||||
|
||||
init(layoutDirection: LayoutDirection, storage: [LayoutSubview], globalOrigin: CGPoint) {
|
||||
self.layoutDirection = layoutDirection
|
||||
self.storage = storage
|
||||
self.globalOrigin = globalOrigin
|
||||
}
|
||||
|
||||
init<R: FiberRenderer>(_ node: FiberReconciler<R>.Fiber) {
|
||||
self.init(
|
||||
layoutDirection: node.outputs.environment.environment.layoutDirection,
|
||||
storage: []
|
||||
storage: [],
|
||||
globalOrigin: node.geometry?.origin.globalOrigin ?? .zero
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -53,7 +58,11 @@ public struct LayoutSubviews: Equatable, RandomAccessCollection {
|
|||
}
|
||||
|
||||
public subscript(bounds: Range<Int>) -> LayoutSubviews {
|
||||
.init(layoutDirection: layoutDirection, storage: .init(storage[bounds]))
|
||||
.init(
|
||||
layoutDirection: layoutDirection,
|
||||
storage: .init(storage[bounds]),
|
||||
globalOrigin: globalOrigin
|
||||
)
|
||||
}
|
||||
|
||||
public subscript<S>(indices: S) -> LayoutSubviews where S: Sequence, S.Element == Int {
|
||||
|
@ -61,7 +70,8 @@ public struct LayoutSubviews: Equatable, RandomAccessCollection {
|
|||
layoutDirection: layoutDirection,
|
||||
storage: storage.enumerated()
|
||||
.filter { indices.contains($0.offset) }
|
||||
.map(\.element)
|
||||
.map(\.element),
|
||||
globalOrigin: globalOrigin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -165,10 +175,13 @@ public struct LayoutSubview: Equatable {
|
|||
guard let fiber = fiber, let element = element else { return }
|
||||
let geometry = ViewGeometry(
|
||||
// Shift to the anchor point in the parent's coordinate space.
|
||||
origin: .init(origin: .init(
|
||||
x: position.x - (dimensions.width * anchor.x),
|
||||
y: position.y - (dimensions.height * anchor.y)
|
||||
)),
|
||||
origin: .init(
|
||||
parent: fiber.elementParent?.geometry?.origin.globalOrigin ?? .zero,
|
||||
origin: .init(
|
||||
x: position.x - (dimensions.width * anchor.x),
|
||||
y: position.y - (dimensions.height * anchor.y)
|
||||
)
|
||||
),
|
||||
dimensions: dimensions,
|
||||
proposal: proposal
|
||||
)
|
||||
|
@ -176,6 +189,10 @@ public struct LayoutSubview: Equatable {
|
|||
if geometry != fiber.alternate?.geometry {
|
||||
caches.mutations.append(.layout(element: element, geometry: geometry))
|
||||
}
|
||||
caches.layoutSubviews[
|
||||
ObjectIdentifier(fiber),
|
||||
default: .init(fiber)
|
||||
].globalOrigin = geometry.origin.globalOrigin
|
||||
// Update ours and our alternate's geometry
|
||||
fiber.geometry = geometry
|
||||
fiber.alternate?.geometry = geometry
|
||||
|
|
|
@ -251,7 +251,7 @@ struct ReconcilePass: FiberReconcilerPass {
|
|||
previous: element,
|
||||
newContent: newContent,
|
||||
geometry: node.fiber?.geometry ?? .init(
|
||||
origin: .init(origin: .zero),
|
||||
origin: .init(parent: .zero, origin: .zero),
|
||||
dimensions: .init(size: .zero, alignmentGuides: [:]),
|
||||
proposal: .unspecified
|
||||
)
|
||||
|
|
|
@ -29,6 +29,9 @@ public struct ViewGeometry: Equatable {
|
|||
|
||||
/// The position of the `View` relative to its parent.
|
||||
public struct ViewOrigin: Equatable {
|
||||
@_spi(TokamakCore)
|
||||
public let parent: CGPoint
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public let origin: CGPoint
|
||||
|
||||
|
@ -36,6 +39,10 @@ public struct ViewOrigin: Equatable {
|
|||
public var x: CGFloat { origin.x }
|
||||
@_spi(TokamakCore)
|
||||
public var y: CGFloat { origin.y }
|
||||
|
||||
public var globalOrigin: CGPoint {
|
||||
parent.offset(by: origin)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ViewDimensions: Equatable {
|
||||
|
|
|
@ -118,6 +118,19 @@ public final class _PreferenceStore: CustomDebugStringConvertible {
|
|||
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key))
|
||||
}
|
||||
|
||||
/// Returns the new value for `Key`, or `nil` if the value did not change.
|
||||
public func newValue<Key>(forKey key: Key.Type = Key.self) -> Key.Value?
|
||||
where Key: PreferenceKey, Key.Value: Equatable
|
||||
{
|
||||
let value = value(forKey: key).value
|
||||
let previousValue = previousValue(forKey: key).value
|
||||
if value != previousValue {
|
||||
return value
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||
where Key: PreferenceKey
|
||||
{
|
||||
|
|
|
@ -230,9 +230,7 @@ public struct DOMFiberRenderer: FiberRenderer {
|
|||
src: bundle?
|
||||
.path(forResource: name, ofType: nil) ?? name
|
||||
) { naturalSize in
|
||||
environment.afterReconcile {
|
||||
image._intrinsicSize = naturalSize
|
||||
}
|
||||
image._intrinsicSize = naturalSize
|
||||
}
|
||||
return .zero
|
||||
case let .resizable(.named(name, bundle: bundle), _, _):
|
||||
|
@ -244,9 +242,7 @@ public struct DOMFiberRenderer: FiberRenderer {
|
|||
src: bundle?
|
||||
.path(forResource: name, ofType: nil) ?? name
|
||||
) { naturalSize in
|
||||
environment.afterReconcile {
|
||||
image._intrinsicSize = naturalSize
|
||||
}
|
||||
image._intrinsicSize = naturalSize
|
||||
}
|
||||
return .zero
|
||||
}
|
||||
|
@ -328,6 +324,38 @@ public struct DOMFiberRenderer: FiberRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
final class Head {
|
||||
let head = document.head.object!
|
||||
var metaTags = [JSObject]()
|
||||
var title: JSObject?
|
||||
}
|
||||
|
||||
private let head = Head()
|
||||
public func preferencesChanged(_ preferenceStore: _PreferenceStore) {
|
||||
if let newMetaTags = preferenceStore.newValue(forKey: HTMLMetaPreferenceKey.self) {
|
||||
for oldTag in head.metaTags {
|
||||
_ = head.head.removeChild!(oldTag)
|
||||
}
|
||||
head.metaTags = newMetaTags.map {
|
||||
let template = document.createElement!("template").object!
|
||||
template.innerHTML = .string($0.outerHTML())
|
||||
let meta = template.content.firstChild.object!
|
||||
_ = head.head.appendChild!(meta)
|
||||
return meta
|
||||
}
|
||||
}
|
||||
if let newTitle = preferenceStore.newValue(forKey: HTMLTitlePreferenceKey.self) {
|
||||
if let title = head.title {
|
||||
title.innerHTML = .string(newTitle)
|
||||
} else {
|
||||
let node = document.createElement!("title").object!
|
||||
_ = head.head.appendChild!(node)
|
||||
node.innerHTML = .string(newTitle)
|
||||
head.title = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let scheduler = JSScheduler()
|
||||
public func schedule(_ action: @escaping () -> ()) {
|
||||
scheduler.schedule(options: nil, action)
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@_spi(TokamakCore) import TokamakCore
|
||||
import Foundation
|
||||
@_spi(TokamakCore)
|
||||
import TokamakCore
|
||||
|
||||
extension _BackgroundStyleModifier: DOMViewModifier {
|
||||
public var isOrderDependent: Bool { true }
|
||||
|
@ -95,12 +97,95 @@ extension _BackgroundStyleModifier: HTMLConvertible,
|
|||
} else {
|
||||
return {
|
||||
$0
|
||||
.visit(_BackgroundLayout(
|
||||
content: content,
|
||||
background: Rectangle().fill(style),
|
||||
alignment: .center
|
||||
))
|
||||
.visit(
|
||||
_BackgroundStyleLayout(
|
||||
style: style,
|
||||
backgroundLayout: _BackgroundLayout(
|
||||
content: content,
|
||||
background: _ShapeView(shape: Rectangle(), style: style),
|
||||
alignment: .center
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct _BackgroundStyleLayout<
|
||||
Content: View,
|
||||
Style: ShapeStyle
|
||||
>: _PrimitiveView, HTMLConvertible, Layout {
|
||||
let style: Style
|
||||
let backgroundLayout: _BackgroundLayout<Content, _ShapeView<Rectangle, Style>>
|
||||
|
||||
@Environment(\.self)
|
||||
var environment
|
||||
@State
|
||||
private var fillsScene = false
|
||||
|
||||
var tag: String { "div" }
|
||||
func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
[:]
|
||||
}
|
||||
|
||||
func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(backgroundLayout.background)
|
||||
visitor.visit(backgroundLayout.content)
|
||||
// If the background reaches the top of the scene, apply a "theme-color".
|
||||
// This matches SwiftUI's behavior where a `_BackgroundStyleModifier` that reaches the top
|
||||
// will extend into the safe area.
|
||||
if fillsScene {
|
||||
var shape = _ShapeStyle_Shape(
|
||||
for: .resolveStyle(levels: 0..<1),
|
||||
in: environment,
|
||||
role: .fill
|
||||
)
|
||||
style._apply(to: &shape)
|
||||
guard let style = shape.result.resolvedStyle(on: shape, in: environment),
|
||||
let color = style.color(at: 0)
|
||||
else { return }
|
||||
visitor.visit(HTMLMeta(
|
||||
name: "theme-color",
|
||||
content: color.cssValue(environment)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
typealias Cache = _BackgroundLayout<Content, _ShapeView<Rectangle, Style>>.Cache
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
backgroundLayout.makeCache(subviews: subviews)
|
||||
}
|
||||
|
||||
func spacing(subviews: LayoutSubviews, cache: inout Cache) -> ViewSpacing {
|
||||
backgroundLayout.spacing(subviews: subviews, cache: &cache)
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
backgroundLayout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache)
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
// If the minY == 0, we are touching the top of the scene.
|
||||
let fillsScene = subviews.globalOrigin.y == 0
|
||||
if fillsScene != self.fillsScene {
|
||||
self.fillsScene = fillsScene
|
||||
}
|
||||
return backgroundLayout.placeSubviews(
|
||||
in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &cache
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,26 @@
|
|||
// Created by Carson Katri on 7/19/20.
|
||||
//
|
||||
|
||||
import TokamakCore
|
||||
@_spi(TokamakCore) import TokamakCore
|
||||
|
||||
extension WindowGroup: SceneDeferredToRenderer {
|
||||
public var deferredBody: AnyView {
|
||||
AnyView(content)
|
||||
}
|
||||
}
|
||||
|
||||
extension _WindowGroupTitle: HTMLConvertible {
|
||||
public var tag: String { "div" }
|
||||
public func attributes(useDynamicLayout: Bool) -> [HTMLAttribute: String] {
|
||||
guard !useDynamicLayout else { return [:] }
|
||||
return ["style": "position: absolute; width: 0; height: 0; top: 0; left: 0;"]
|
||||
}
|
||||
|
||||
public func primitiveVisitor<V>(useDynamicLayout: Bool) -> ((V) -> ())? where V: ViewVisitor {
|
||||
{
|
||||
if let title = self.title {
|
||||
$0.visit(HTMLTitle(_TextProxy(title).rawText))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,22 +237,38 @@ public struct StaticHTMLFiberRenderer: FiberRenderer {
|
|||
|
||||
public func render<A: App>(_ app: A) -> String {
|
||||
_ = FiberReconciler(self, app)
|
||||
return """
|
||||
return renderedHTML()
|
||||
}
|
||||
|
||||
public func render<V: View>(_ view: V) -> String {
|
||||
_ = FiberReconciler(self, view)
|
||||
return renderedHTML()
|
||||
}
|
||||
|
||||
private func renderedHTML() -> String {
|
||||
"""
|
||||
<!doctype html>
|
||||
<head>
|
||||
\(head.title != nil ? "<title>\(head.title!)</title>" : "")
|
||||
\(head.metaTags.joined(separator: "\n"))
|
||||
</head>
|
||||
<html>
|
||||
\(rootElement.description)
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
|
||||
public func render<V: View>(_ view: V) -> String {
|
||||
_ = FiberReconciler(self, view)
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
\(rootElement.description)
|
||||
</html>
|
||||
"""
|
||||
private final class Head {
|
||||
var metaTags = [String]()
|
||||
var title: String?
|
||||
}
|
||||
|
||||
private let head = Head()
|
||||
public func preferencesChanged(_ preferenceStore: _PreferenceStore) {
|
||||
head.metaTags = preferenceStore.value(forKey: HTMLMetaPreferenceKey.self).value.map {
|
||||
$0.outerHTML()
|
||||
}
|
||||
head.title = preferenceStore.value(forKey: HTMLTitlePreferenceKey.self).value
|
||||
}
|
||||
|
||||
public func schedule(_ action: @escaping () -> ()) {
|
||||
|
|
|
@ -58,7 +58,7 @@ struct HTMLBody: AnyHTML {
|
|||
]
|
||||
}
|
||||
|
||||
extension HTMLMeta.MetaTag {
|
||||
public extension HTMLMeta.MetaTag {
|
||||
func outerHTML() -> String {
|
||||
switch self {
|
||||
case let .charset(charset):
|
||||
|
|
|
@ -98,17 +98,17 @@ final class FrameTests: XCTestCase {
|
|||
for (nativeVertical, tokamakVertical) in SwiftUI.VerticalAlignment.allCases {
|
||||
await compare(size: .init(width: 500, height: 500)) {
|
||||
SwiftUI.Rectangle()
|
||||
.fill(SwiftUI.Color(white: 0))
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
maxHeight: .infinity,
|
||||
alignment: .init(horizontal: nativeHorizontal, vertical: nativeVertical)
|
||||
)
|
||||
.background(Color(white: 127 / 255))
|
||||
.background(Rectangle().fill(Color(white: 127 / 255)))
|
||||
} to: {
|
||||
TokamakStaticHTML.Rectangle()
|
||||
.fill(TokamakStaticHTML.Color(white: 0))
|
||||
.fill(Color(white: 0))
|
||||
.frame(width: 100, height: 100)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
|
@ -118,7 +118,7 @@ final class FrameTests: XCTestCase {
|
|||
vertical: tokamakVertical
|
||||
)
|
||||
)
|
||||
.background(Color(white: 127 / 255))
|
||||
.background(Rectangle().fill(Color(white: 127 / 255)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue