Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support <meta>/<title> in Fiber renderers #503

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Sources/TokamakCore/App/Scenes/WindowGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
// Created by Carson Katri on 7/16/20.
//

@_spi(TokamakCore)
public struct _WindowGroupTitle: _PrimitiveView {
public let title: Text?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be an optional?

In WindowGroups where title is nil, it should be more efficient to not use a _WindowGroupTitle at all.

}

public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
public let id: String
public let title: Text?
Expand Down Expand Up @@ -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
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
3 changes: 2 additions & 1 deletion Sources/TokamakCore/Fiber/Fiber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,8 @@ public extension FiberReconciler {
environment: .init(rootEnvironment),
traits: .init(),
preferenceStore: preferences
)
),
preferenceStore: preferences ?? .init()
)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
Expand Down
35 changes: 6 additions & 29 deletions Sources/TokamakCore/Fiber/FiberReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,13 @@ 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
/// to ensure all preference values are passed down correctly.
/// 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
Expand All @@ -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
}

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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 }
}
}
5 changes: 5 additions & 0 deletions Sources/TokamakCore/Fiber/FiberRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -107,6 +110,8 @@ public extension FiberRenderer {
}
}

func preferencesChanged(_ preferenceStore: _PreferenceStore) {}

@discardableResult
@_disfavoredOverload
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
Expand Down
33 changes: 25 additions & 8 deletions Sources/TokamakCore/Fiber/Layout/LayoutSubviews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}

Expand All @@ -53,15 +58,20 @@ 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 {
.init(
layoutDirection: layoutDirection,
storage: storage.enumerated()
.filter { indices.contains($0.offset) }
.map(\.element)
.map(\.element),
globalOrigin: globalOrigin
)
}
}
Expand Down Expand Up @@ -165,17 +175,24 @@ 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
)
// Push a layout mutation if needed.
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
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Fiber/Passes/ReconcilePass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
7 changes: 7 additions & 0 deletions Sources/TokamakCore/Fiber/ViewGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,20 @@ 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

@_spi(TokamakCore)
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 {
Expand Down
13 changes: 13 additions & 0 deletions Sources/TokamakCore/Preferences/PreferenceKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
40 changes: 34 additions & 6 deletions Sources/TokamakDOM/DOMFiberRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,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), _, _):
Expand All @@ -239,9 +237,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
}
Expand Down Expand Up @@ -323,6 +319,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)
Expand Down
Loading