Compare commits

..

1 Commits
main ... v0.10

Author SHA1 Message Date
Max Desiatov 0ccb84afd5 Fix build issue with SwiftWasm 5.7 2022-08-31 12:51:32 +01:00
207 changed files with 580 additions and 11066 deletions

View File

@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: bug
assignees: carson-katri assignees: MaxDesiatov
--- ---
@ -28,17 +28,17 @@ A clear and concise description of what you expected to happen.
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference. If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. macOS 12.4] - OS: [e.g. macOS]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Version of the browser [e.g. 22] - Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.10.1] - Version of Tokamak [e.g. 0.6.1]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone 6] - Device: [e.g. iPhone6]
- OS: [e.g. iOS15.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari] - Browser [e.g. stock browser, safari]
- Version of the browser [e.g. 22] - Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.10.1] - Version of Tokamak [e.g. 0.6.1]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@ -14,88 +14,76 @@ jobs:
- uses: swiftwasm/swiftwasm-action@v5.6 - uses: swiftwasm/swiftwasm-action@v5.6
with: with:
shell-action: carton bundle --product TokamakDemo shell-action: carton bundle --product TokamakDemo
- name: Check binary size
shell: bash
run: |
ls -la Bundle
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
swiftwasm_test: swiftwasm_test_5_6:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy:
fail-fast: true
matrix:
include:
- { toolchain: wasm-5.6.0-RELEASE }
- { toolchain: wasm-5.7-SNAPSHOT-2022-07-27-a }
- { toolchain: wasm-DEVELOPMENT-SNAPSHOT-2022-07-23-a }
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: echo "${{ matrix.toolchain }}" > .swift-version
- uses: swiftwasm/swiftwasm-action@v5.6 - uses: swiftwasm/swiftwasm-action@v5.6
with: with:
shell-action: carton test --environment node shell-action: carton test
core_macos_build: # Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
runs-on: macos-12 # core_macos_build:
# runs-on: macos-11
steps: # steps:
- uses: actions/checkout@v2 # - uses: actions/checkout@v2
- name: Run the test suite on macOS, build the demo project for iOS # - name: Run the test suite on macOS, build the demo project for iOS
shell: bash # shell: bash
run: | # run: |
set -ex # set -ex
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/ # sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
# avoid building unrelated products for testing by specifying the test product explicitly # # avoid building unrelated products for testing by specifying the test product explicitly
swift build --product TokamakPackageTests # swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest || # `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1) # (cp -r /var/folders/*/*/*/*Tests . ; exit 1)
rm -rf Sources/TokamakGTKCHelpers/*.c # rm -rf Sources/TokamakGTKCHelpers/*.c
xcodebuild -version # xcodebuild -version
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works. # # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \ # # Disable macOS builds until Monterey is available on GHA.
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \ # # xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
xcpretty --color # # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
# # xcpretty --color
cd "NativeDemo" # cd "NativeDemo"
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \ # xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \ # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
xcpretty --color # xcpretty --color
cd .. # cd ..
./benchmark.sh # ./benchmark.sh
- name: Upload failed snapshots # - name: Upload failed snapshots
uses: actions/upload-artifact@v2 # uses: actions/upload-artifact@v2
if: ${{ failure() }} # if: ${{ failure() }}
with: # with:
name: Failed snapshots # name: Failed snapshots
path: '*Tests' # path: '*Tests'
# FIXME: disabled due to build errors, to be investigated # gtk_macos_build:
# gtk_macos_build: # runs-on: macos-11
# runs-on: macos-12
# # steps:
# steps: # - uses: actions/checkout@v2
# - uses: actions/checkout@v2 # - name: Build the GTK renderer on macOS
# - name: Build the GTK renderer on macOS # shell: bash
# shell: bash # run: |
# run: | # set -ex
# set -ex # sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
# # brew install gtk+3
# brew install gtk+3
# # make build
# make build
gtk_ubuntu_18_04_build: gtk_ubuntu_18_04_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: swiftlang/swift:nightly-5.7-bionic image: swiftlang/swift:nightly-bionic
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -110,7 +98,7 @@ jobs:
gtk_ubuntu_20_04_build: gtk_ubuntu_20_04_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: swiftlang/swift:nightly-5.7-focal image: swiftlang/swift:nightly-focal
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
codecov: codecov:
container: container:
image: swiftlang/swift:nightly-5.7-focal image: swiftlang/swift:nightly-focal
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0 - run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0

View File

@ -24,7 +24,6 @@ jobs:
dependencies, dependencies,
documentation, documentation,
enhancement, enhancement,
Fiber,
refactor, refactor,
SwiftUI compatibility, SwiftUI compatibility,
test suite, test suite,

View File

@ -8,7 +8,5 @@
--maxwidth 100 --maxwidth 100
--wraparguments before-first --wraparguments before-first
--funcattributes prev-line --funcattributes prev-line
--typeattributes prev-line
--varattributes prev-line
--disable andOperator --disable andOperator
--swiftversion 5.6 --swiftversion 5.4

View File

@ -29,19 +29,6 @@ only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conj
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
`TokamakAndroid` with no need to be aware of any of the web modules. `TokamakAndroid` with no need to be aware of any of the web modules.
### Testing
Tokamak uses [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library to
make sure that HTML and layout generated by renderers stay consistent. To run the test suite on macOS
you should use this command:
```sh
swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
```
Unfortunately, plain `swift test` won't work as it tries to build targets that aren't related to
the test suite.
### Coding Style ### Coding Style
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and

View File

@ -46,23 +46,23 @@ let package = Package(
], ],
dependencies: [ dependencies: [
.package( .package(
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git", url: "https://github.com/swiftwasm/JavaScriptKit.git",
from: "0.15.0" from: "0.15.0"
), ),
.package( .package(
url: "https://gitlink.org.cn/dnrops/OpenCombine.git", url: "https://github.com/OpenCombine/OpenCombine.git",
from: "0.12.0" from: "0.12.0"
), ),
.package( .package(
url: "https://gitcode.net/dnrops/OpenCombineJS.git", url: "https://github.com/swiftwasm/OpenCombineJS.git",
from: "0.2.0" from: "0.2.0"
), ),
.package( .package(
url: "https://gitlink.org.cn/dnrops/swift-benchmark", url: "https://github.com/google/swift-benchmark",
from: "0.1.2" from: "0.1.2"
), ),
.package( .package(
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git", url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
from: "1.9.0" from: "1.9.0"
), ),
], ],
@ -135,7 +135,6 @@ let package = Package(
dependencies: [ dependencies: [
.product(name: "Benchmark", package: "swift-benchmark"), .product(name: "Benchmark", package: "swift-benchmark"),
"TokamakCore", "TokamakCore",
"TokamakTestRenderer",
] ]
), ),
.executableTarget( .executableTarget(
@ -195,25 +194,6 @@ let package = Package(
name: "TokamakTestRenderer", name: "TokamakTestRenderer",
dependencies: ["TokamakCore"] dependencies: ["TokamakCore"]
), ),
.testTarget(
name: "TokamakLayoutTests",
dependencies: [
"TokamakCore",
"TokamakStaticHTML",
.product(
name: "SnapshotTesting",
package: "swift-snapshot-testing",
condition: .when(platforms: [.macOS])
),
]
),
.testTarget(
name: "TokamakReconcilerTests",
dependencies: [
"TokamakCore",
"TokamakTestRenderer",
]
),
.testTarget( .testTarget(
name: "TokamakTests", name: "TokamakTests",
dependencies: ["TokamakTestRenderer"] dependencies: ["TokamakTestRenderer"]

View File

@ -138,31 +138,6 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
localized date formatting (or any arbitrary style/script/font added that way) are available in your localized date formatting (or any arbitrary style/script/font added that way) are available in your
app. app.
### Fiber renderers
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
is optionally available. It can provide faster updates and allow for larger View hierarchies.
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
You can specify which reconciler to use in your `App`'s configuration:
```swift
struct CounterApp: App {
static let _configuration: _AppConfiguration = .init(
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
reconciler: .fiber(useDynamicLayout: true)
)
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
```
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
## Requirements ## Requirements
### For app developers ### For app developers
@ -172,7 +147,7 @@ struct CounterApp: App {
and macOS at the same time. and macOS at the same time.
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux. - [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
Other Linux distributions are currently not supported. Other Linux distributions are currently not supported.
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps) - [`carton` 0.14.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
### For users of apps depending on Tokamak ### For users of apps depending on Tokamak
@ -241,9 +216,6 @@ carton dev
You can also clone this repository and run `carton dev --product TokamakDemo` in its root You can also clone this repository and run `carton dev --product TokamakDemo` in its root
directory. This will build the demo app that shows almost all of the currently implemented APIs. directory. This will build the demo app that shows almost all of the currently implemented APIs.
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
## Security ## Security
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
@ -325,10 +297,9 @@ appreciated and helps in maintaining the project.
## Maintainers ## Maintainers
In alphabetical order: [Carson Katri](https://github.com/carson-katri), In alphabetical order: [Carson Katri](https://github.com/carson-katri),
[Ezra Berch](https://github.com/ezraberch), [David Hunt](https://github.com/foscomputerservices), [Ezra Berch](https://github.com/ezraberch),
[Jed Fox](https://jedfox.com), [Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
[Yuta Saito](https://github.com/kateinoigakukun/).
## Acknowledgments ## Acknowledgments

View File

@ -40,20 +40,14 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
} }
} }
@frozen @frozen public struct EmptyAnimatableData: VectorArithmetic {
public struct EmptyAnimatableData: VectorArithmetic {
@inlinable @inlinable
public init() {} public init() {}
@inlinable public static var zero: Self { .init() }
@inlinable
public static var zero: Self { .init() }
@inlinable @inlinable
public static func += (lhs: inout Self, rhs: Self) {} public static func += (lhs: inout Self, rhs: Self) {}
@inlinable @inlinable
public static func -= (lhs: inout Self, rhs: Self) {} public static func -= (lhs: inout Self, rhs: Self) {}
@inlinable @inlinable
public static func + (lhs: Self, rhs: Self) -> Self { public static func + (lhs: Self, rhs: Self) -> Self {
.zero .zero
@ -66,15 +60,11 @@ public struct EmptyAnimatableData: VectorArithmetic {
@inlinable @inlinable
public mutating func scale(by rhs: Double) {} public mutating func scale(by rhs: Double) {}
@inlinable public var magnitudeSquared: Double { .zero }
@inlinable
public var magnitudeSquared: Double { .zero }
public static func == (a: Self, b: Self) -> Bool { true } public static func == (a: Self, b: Self) -> Bool { true }
} }
@frozen @frozen public struct AnimatablePair<First, Second>: VectorArithmetic
public struct AnimatablePair<First, Second>: VectorArithmetic
where First: VectorArithmetic, Second: VectorArithmetic where First: VectorArithmetic, Second: VectorArithmetic
{ {
public var first: First public var first: First
@ -91,8 +81,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
set { (first, second) = newValue } set { (first, second) = newValue }
} }
@_transparent @_transparent public static var zero: Self {
public static var zero: Self {
@_transparent get { @_transparent get {
.init(First.zero, Second.zero) .init(First.zero, Second.zero)
} }
@ -126,8 +115,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
second.scale(by: rhs) second.scale(by: rhs)
} }
@_transparent @_transparent public var magnitudeSquared: Double {
public var magnitudeSquared: Double {
@_transparent get { @_transparent get {
first.magnitudeSquared + second.magnitudeSquared first.magnitudeSquared + second.magnitudeSquared
} }

View File

@ -139,8 +139,7 @@ public struct _AnimationProxy {
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() } public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
} }
@frozen @frozen public struct _AnimationModifier<Value>: ViewModifier, Equatable
public struct _AnimationModifier<Value>: ViewModifier, Equatable
where Value: Equatable where Value: Equatable
{ {
public var animation: Animation? public var animation: Animation?
@ -156,9 +155,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
let content: Content let content: Content
let animation: Animation? let animation: Animation?
let value: Value let value: Value
@State private var lastValue: Value?
@State
private var lastValue: Value?
var body: some View { var body: some View {
content.transaction { content.transaction {
@ -183,8 +180,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
} }
} }
@frozen @frozen public struct _AnimationView<Content>: View
public struct _AnimationView<Content>: View
where Content: Equatable, Content: View where Content: Equatable, Content: View
{ {
public var content: Content public var content: Content

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
public struct Transaction { public struct Transaction {
/// The overridden transaction for a state change in a `withTransaction` block. /// The overriden transaction for a state change in a `withTransaction` block.
/// Is always set back to `nil` when the block exits. /// Is always set back to `nil` when the block exits.
static var _active: Self? static var _active: Self?
@ -50,8 +50,7 @@ protocol _TransactionModifierProtocol {
func modifyTransaction(_ transaction: inout Transaction) func modifyTransaction(_ transaction: inout Transaction)
} }
@frozen @frozen public struct _TransactionModifier: ViewModifier {
public struct _TransactionModifier: ViewModifier {
public var transform: (inout Transaction) -> () public var transform: (inout Transaction) -> ()
@inlinable @inlinable
@ -78,8 +77,7 @@ extension ModifiedContent: _TransactionModifierProtocol
} }
} }
@frozen @frozen public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
public var content: V public var content: V
public var base: _TransactionModifier public var base: _TransactionModifier

View File

@ -25,9 +25,7 @@ public protocol VectorArithmetic: AdditiveArithmetic {
extension Float: VectorArithmetic { extension Float: VectorArithmetic {
@_transparent @_transparent
public mutating func scale(by rhs: Double) { self *= Float(rhs) } public mutating func scale(by rhs: Double) { self *= Float(rhs) }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) } @_transparent get { Double(self * self) }
} }
} }
@ -35,9 +33,7 @@ extension Float: VectorArithmetic {
extension Double: VectorArithmetic { extension Double: VectorArithmetic {
@_transparent @_transparent
public mutating func scale(by rhs: Double) { self *= rhs } public mutating func scale(by rhs: Double) { self *= rhs }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { self * self } @_transparent get { self * self }
} }
} }
@ -45,9 +41,7 @@ extension Double: VectorArithmetic {
extension CGFloat: VectorArithmetic { extension CGFloat: VectorArithmetic {
@_transparent @_transparent
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) } public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) } @_transparent get { Double(self * self) }
} }
} }

View File

@ -21,7 +21,7 @@ import Foundation
public protocol _AnimationSolver { public protocol _AnimationSolver {
/// Solve value at a specific point in time. /// Solve value at a specific point in time.
func solve(at t: Double) -> Double func solve(at t: Double) -> Double
/// Calculates the duration of the animation to a specific precision. /// Calculates the duration of the animation to a specific presision.
func restingPoint(precision y: Double) -> Double func restingPoint(precision y: Double) -> Double
} }

View File

@ -1,165 +0,0 @@
// Copyright 2022 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.
/// A type-eraser for `VectorArithmetic`.
public struct _AnyAnimatableData: VectorArithmetic {
private var box: _AnyAnimatableDataBox?
private init(_ box: _AnyAnimatableDataBox?) {
self.box = box
}
}
/// A box for vector arithmetic types.
///
/// Conforming types are only expected to handle value types (enums and structs).
/// Classes aren't really mutable so that scaling, but even then subclassing is impossible,
/// at least in my attempts. Also `VectorArithmetic` does not have a self-conforming
/// existential. Thus the problem of two types being equal but not sharing a common
/// supertype is avoided. Consider a type `Super` that has subtypes `A : Super` and
/// `B : Super`; casting both `A.self as? B.Type` and `B.self as? A.Type` fail.
/// This is important for static operators, since non-type-erased operators get this right.
/// Thankfully, only no-inheritance types are supported.
private protocol _AnyAnimatableDataBox {
var value: Any { get }
func equals(_ other: Any) -> Bool
func add(_ other: Any) -> _AnyAnimatableDataBox
func subtract(_ other: Any) -> _AnyAnimatableDataBox
mutating func scale(by scalar: Double)
var magnitudeSquared: Double { get }
}
private struct _ConcreteAnyAnimatableDataBox<
Base: VectorArithmetic
>: _AnyAnimatableDataBox {
var base: Base
var value: Any {
base
}
// MARK: Equatable
func equals(_ other: Any) -> Bool {
guard let other = other as? Base else {
return false
}
return base == other
}
// MARK: AdditiveArithmetic
func add(_ other: Any) -> _AnyAnimatableDataBox {
guard let other = other as? Base else {
// TODO: Look into whether this should crash.
// SwiftUI didn't crash on the first beta.
return self
}
return Self(base: base + other)
}
func subtract(_ other: Any) -> _AnyAnimatableDataBox {
guard let other = other as? Base else {
// TODO: Look into whether this should crash.
// SwiftUI didn't crash on the first beta.
return self
}
return Self(base: base - other)
}
// MARK: VectorArithmetic
mutating func scale(by scalar: Double) {
base.scale(by: scalar)
}
var magnitudeSquared: Double {
base.magnitudeSquared
}
}
public extension _AnyAnimatableData {
// MARK: Equatable
static func == (lhs: Self, rhs: Self) -> Bool {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return rhsBox.equals(lhsBox.value)
case (.some, nil), (nil, .some):
return false
case (nil, nil):
return true
}
}
// MARK: AdditiveArithmetic
static func + (lhs: Self, rhs: Self) -> Self {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return Self(rhsBox.add(lhsBox.value))
case (let box?, nil), (nil, let box?):
return Self(box)
case (nil, nil):
return lhs
}
}
static func - (lhs: Self, rhs: Self) -> Self {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return Self(rhsBox.subtract(lhsBox.value))
case (let box?, nil), (nil, let box?):
return Self(box)
case (nil, nil):
return lhs
}
}
static var zero: _AnyAnimatableData {
_AnyAnimatableData(nil)
}
// MARK: VectorArithmetic
mutating func scale(by rhs: Double) {
box?.scale(by: rhs)
}
var magnitudeSquared: Double {
box?.magnitudeSquared ?? 0
}
}
public extension _AnyAnimatableData {
init<Data: VectorArithmetic>(_ data: Data) {
box = _ConcreteAnyAnimatableDataBox(base: data)
}
var value: Any {
box?.value ?? ()
}
}

View File

@ -20,8 +20,7 @@ import Foundation
public protocol _VectorMath: Animatable {} public protocol _VectorMath: Animatable {}
public extension _VectorMath { public extension _VectorMath {
@inlinable @inlinable var magnitude: Double {
var magnitude: Double {
animatableData.magnitudeSquared.squareRoot() animatableData.magnitudeSquared.squareRoot()
} }

View File

@ -28,10 +28,7 @@ public protocol App: _TitledApp {
var body: Body { get } var body: Body { get }
/// Implemented by the renderer to mount the `App` /// Implemented by the renderer to mount the `App`
static func _launch( static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
_ app: Self,
with configuration: _AppConfiguration
)
/// Implemented by the renderer to update the `App` on `ScenePhase` changes /// Implemented by the renderer to update the `App` on `ScenePhase` changes
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get } var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
@ -39,38 +36,14 @@ public protocol App: _TitledApp {
/// Implemented by the renderer to update the `App` on `ColorScheme` changes /// Implemented by the renderer to update the `App` on `ColorScheme` changes
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get } var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
static var _configuration: _AppConfiguration { get }
static func main() static func main()
init() init()
} }
public struct _AppConfiguration {
public let reconciler: Reconciler
public let rootEnvironment: EnvironmentValues
public init(
reconciler: Reconciler = .stack,
rootEnvironment: EnvironmentValues = .init()
) {
self.reconciler = reconciler
self.rootEnvironment = rootEnvironment
}
public enum Reconciler {
/// Use the `StackReconciler`.
case stack
/// Use the `FiberReconciler` with layout steps optionally enabled.
case fiber(useDynamicLayout: Bool = false)
}
}
public extension App { public extension App {
static var _configuration: _AppConfiguration { .init() }
static func main() { static func main() {
let app = Self() let app = Self()
_launch(app, with: Self._configuration) _launch(app, EnvironmentValues())
} }
} }

View File

@ -17,13 +17,9 @@
import OpenCombineShim import OpenCombineShim
@propertyWrapper @propertyWrapper public struct AppStorage<Value>: DynamicProperty {
public struct AppStorage<Value>: DynamicProperty {
let provider: _StorageProvider? let provider: _StorageProvider?
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
@Environment(\._defaultAppStorage)
var defaultProvider: _StorageProvider?
var unwrappedProvider: _StorageProvider { var unwrappedProvider: _StorageProvider {
provider ?? defaultProvider! provider ?? defaultProvider!
} }

View File

@ -21,23 +21,8 @@ public protocol Scene {
// FIXME: If I put `@SceneBuilder` in front of this // FIXME: If I put `@SceneBuilder` in front of this
// it fails to build with no useful error message. // it fails to build with no useful error message.
var body: Self.Body { get } var body: Self.Body { get }
/// Override the default implementation for `Scene`s with body types of `Never`
/// or in cases where the body would normally need to be type erased.
///
/// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor`
func _visitChildren<V: SceneVisitor>(_ visitor: V)
/// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom
/// `LayoutComputer` from the `SceneInputs`.
///
/// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`.
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs
} }
public typealias SceneInputs<S: Scene> = ViewInputs<S>
public typealias SceneOutputs = ViewOutputs
protocol TitledScene { protocol TitledScene {
var title: Text? { get } var title: Text? { get }
} }

View File

@ -29,14 +29,7 @@ public extension SceneBuilder {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene, static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
C1: Scene C1: Scene
{ {
_TupleScene( _TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
(c0, c1),
children: [_AnyScene(c0), _AnyScene(c1)],
visit: {
$0.visit(c0)
$0.visit(c1)
}
)
} }
} }
@ -44,15 +37,7 @@ public extension SceneBuilder {
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
where C0: Scene, C1: Scene, C2: Scene where C0: Scene, C1: Scene, C2: Scene
{ {
_TupleScene( _TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
(c0, c1, c2),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
}
)
} }
} }
@ -65,13 +50,7 @@ public extension SceneBuilder {
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
_TupleScene( _TupleScene(
(c0, c1, c2, c3), (c0, c1, c2, c3),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)], children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
}
) )
} }
} }
@ -86,14 +65,7 @@ public extension SceneBuilder {
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
_TupleScene( _TupleScene(
(c0, c1, c2, c3, c4), (c0, c1, c2, c3, c4),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)], children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
}
) )
} }
} }
@ -118,15 +90,7 @@ public extension SceneBuilder {
_AnyScene(c3), _AnyScene(c3),
_AnyScene(c4), _AnyScene(c4),
_AnyScene(c5), _AnyScene(c5),
], ]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
}
) )
} }
} }
@ -153,16 +117,7 @@ public extension SceneBuilder {
_AnyScene(c4), _AnyScene(c4),
_AnyScene(c5), _AnyScene(c5),
_AnyScene(c6), _AnyScene(c6),
], ]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
}
) )
} }
} }
@ -191,17 +146,7 @@ public extension SceneBuilder {
_AnyScene(c5), _AnyScene(c5),
_AnyScene(c6), _AnyScene(c6),
_AnyScene(c7), _AnyScene(c7),
], ]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
}
) )
} }
} }
@ -232,18 +177,7 @@ public extension SceneBuilder {
_AnyScene(c6), _AnyScene(c6),
_AnyScene(c7), _AnyScene(c7),
_AnyScene(c8), _AnyScene(c8),
], ]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
}
) )
} }
} }
@ -276,19 +210,7 @@ public extension SceneBuilder {
_AnyScene(c7), _AnyScene(c7),
_AnyScene(c8), _AnyScene(c8),
_AnyScene(c9), _AnyScene(c9),
], ]
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
$0.visit(c9)
}
) )
} }
} }

View File

@ -23,8 +23,7 @@ public enum _DefaultSceneStorageProvider {
public static var `default`: _StorageProvider! public static var `default`: _StorageProvider!
} }
@propertyWrapper @propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
public struct SceneStorage<Value>: DynamicProperty {
let key: String let key: String
let defaultValue: Value let defaultValue: Value
let store: (_StorageProvider, String, Value) -> () let store: (_StorageProvider, String, Value) -> ()

View File

@ -75,8 +75,4 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
// public init(_ titleKey: LocalizedStringKey, // public init(_ titleKey: LocalizedStringKey,
// @ViewBuilder content: () -> Content) { // @ViewBuilder content: () -> Content) {
// } // }
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
visitor.visit(content)
}
} }

View File

@ -42,7 +42,7 @@ public struct _AnyApp: App {
} }
@_spi(TokamakCore) @_spi(TokamakCore)
public static func _launch(_ app: Self, with configuration: _AppConfiguration) { public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.") fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
} }
@ -51,10 +51,6 @@ public struct _AnyApp: App {
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.") fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
} }
public static var _configuration: _AppConfiguration {
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
}
@_spi(TokamakCore) @_spi(TokamakCore)
public var _phasePublisher: AnyPublisher<ScenePhase, Never> { public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.") fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")

View File

@ -17,17 +17,11 @@
struct _TupleScene<T>: Scene, GroupScene { struct _TupleScene<T>: Scene, GroupScene {
let value: T let value: T
let children: [_AnyScene] var children: [_AnyScene]
let visit: (SceneVisitor) -> ()
init( init(_ value: T, children: [_AnyScene]) {
_ value: T,
children: [_AnyScene],
visit: @escaping (SceneVisitor) -> ()
) {
self.value = value self.value = value
self.children = children self.children = children
self.visit = visit
} }
var body: Never { var body: Never {

View File

@ -23,8 +23,7 @@ protocol EnvironmentReader {
mutating func setContent(from values: EnvironmentValues) mutating func setContent(from values: EnvironmentValues)
} }
@propertyWrapper @propertyWrapper public struct Environment<Value>: DynamicProperty {
public struct Environment<Value>: DynamicProperty {
enum Content { enum Content {
case keyPath(KeyPath<EnvironmentValues, Value>) case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value) case value(Value)

View File

@ -17,24 +17,11 @@ public protocol EnvironmentKey {
static var defaultValue: Value { get } static var defaultValue: Value { get }
} }
/// This protocol defines a type which mutates the environment in some way. protocol EnvironmentModifier {
/// Unlike `EnvironmentalModifier`, which reads the environment to
/// create a `ViewModifier`.
///
/// It can be applied to a `View` or `ViewModifier`.
public protocol _EnvironmentModifier {
func modifyEnvironment(_ values: inout EnvironmentValues) func modifyEnvironment(_ values: inout EnvironmentValues)
} }
public extension ViewModifier where Self: _EnvironmentModifier { public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
var environment = inputs.environment.environment
inputs.content.modifyEnvironment(&environment)
return .init(inputs: inputs, environment: environment)
}
}
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentModifier {
public let keyPath: WritableKeyPath<EnvironmentValues, Value> public let keyPath: WritableKeyPath<EnvironmentValues, Value>
public let value: Value public let value: Value
@ -45,7 +32,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentM
public typealias Body = Never public typealias Body = Never
public func modifyEnvironment(_ values: inout EnvironmentValues) { func modifyEnvironment(_ values: inout EnvironmentValues) {
values[keyPath: keyPath] = value values[keyPath: keyPath] = value
} }
} }

View File

@ -17,12 +17,10 @@
import OpenCombineShim import OpenCombineShim
@propertyWrapper @propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
public struct EnvironmentObject<ObjectType>: DynamicProperty
where ObjectType: ObservableObject where ObjectType: ObservableObject
{ {
@dynamicMemberLookup @dynamicMemberLookup public struct Wrapper {
public struct Wrapper {
internal let root: ObjectType internal let root: ObjectType
public subscript<Subject>( public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject> dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>

View File

@ -76,7 +76,7 @@ public extension EnvironmentValues {
} }
} }
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier { struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
let environmentValues: EnvironmentValues let environmentValues: EnvironmentValues
func body(content: Content) -> some View { func body(content: Content) -> some View {

View File

@ -1,153 +0,0 @@
// Copyright 2022 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 2/18/22.
//
import Foundation
/// Used to identify an alignment guide.
///
/// Typically, you would define an alignment guide inside
/// an extension on `HorizontalAlignment` or `VerticalAlignment`:
///
/// extension HorizontalAlignment {
/// private enum MyAlignmentGuide: AlignmentID {
/// static func defaultValue(in context: ViewDimensions) -> CGFloat {
/// return 0.0
/// }
/// }
/// public static let myAlignmentGuide = Self(MyAlignmentGuide.self)
/// }
///
/// Which you can then use with the `alignmentGuide` modifier:
///
/// VStack(alignment: .myAlignmentGuide) {
/// Text("Align Leading")
/// .border(.red)
/// .alignmentGuide(.myAlignmentGuide) { $0[.leading] }
/// Text("Align Trailing")
/// .border(.blue)
/// .alignmentGuide(.myAlignmentGuide) { $0[.trailing] }
/// }
/// .border(.green)
public protocol AlignmentID {
/// The default value for this alignment guide
/// when not set via the `alignmentGuide` modifier.
static func defaultValue(in context: ViewDimensions) -> CGFloat
}
/// An alignment position along the horizontal axis.
@frozen
public struct HorizontalAlignment: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
let id: AlignmentID.Type
public init(_ id: AlignmentID.Type) {
self.id = id
}
}
extension HorizontalAlignment {
public static let leading = Self(Leading.self)
private enum Leading: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
}
}
public static let center = Self(Center.self)
private enum Center: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.width / 2
}
}
public static let trailing = Self(Trailing.self)
private enum Trailing: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.width
}
}
}
@frozen
public struct VerticalAlignment: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
let id: AlignmentID.Type
public init(_ id: AlignmentID.Type) {
self.id = id
}
}
extension VerticalAlignment {
public static let top = Self(Top.self)
private enum Top: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
}
}
public static let center = Self(Center.self)
private enum Center: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height / 2
}
}
public static let bottom = Self(Bottom.self)
private enum Bottom: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context.height
}
}
// TODO: Add baseline vertical alignment guides.
// public static let firstTextBaseline: VerticalAlignment
// public static let lastTextBaseline: VerticalAlignment
}
/// An alignment in both axes.
public struct Alignment: Equatable {
public var horizontal: HorizontalAlignment
public var vertical: VerticalAlignment
public init(
horizontal: HorizontalAlignment,
vertical: VerticalAlignment
) {
self.horizontal = horizontal
self.vertical = vertical
}
public static let topLeading = Self(horizontal: .leading, vertical: .top)
public static let top = Self(horizontal: .center, vertical: .top)
public static let topTrailing = Self(horizontal: .trailing, vertical: .top)
public static let leading = Self(horizontal: .leading, vertical: .center)
public static let center = Self(horizontal: .center, vertical: .center)
public static let trailing = Self(horizontal: .trailing, vertical: .center)
public static let bottomLeading = Self(horizontal: .leading, vertical: .bottom)
public static let bottom = Self(horizontal: .center, vertical: .bottom)
public static let bottomTrailing = Self(horizontal: .trailing, vertical: .bottom)
}

View File

@ -1,21 +0,0 @@
// Copyright 2022 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 5/31/22.
//
/// A type that can visit an `App`.
public protocol AppVisitor: ViewVisitor {
func visit<A: App>(_ app: A)
}

View File

@ -1,65 +0,0 @@
// Copyright 2022 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 5/31/22.
//
import Foundation
public extension FiberReconciler.Fiber {
enum Content {
/// The underlying `App` instance and a function to visit it generically.
case app(Any, visit: (AppVisitor) -> ())
/// The underlying `Scene` instance and a function to visit it generically.
case scene(Any, visit: (SceneVisitor) -> ())
/// The underlying `View` instance and a function to visit it generically.
case view(Any, visit: (ViewVisitor) -> ())
}
/// Create a `Content` value for a given `App`.
func content<A: App>(for app: A) -> Content {
.app(
app,
visit: { [weak self] in
guard case let .app(app, _) = self?.content else { return }
// swiftlint:disable:next force_cast
$0.visit(app as! A)
}
)
}
/// Create a `Content` value for a given `Scene`.
func content<S: Scene>(for scene: S) -> Content {
.scene(
scene,
visit: { [weak self] in
guard case let .scene(scene, _) = self?.content else { return }
// swiftlint:disable:next force_cast
$0.visit(scene as! S)
}
)
}
/// Create a `Content` value for a given `View`.
func content<V: View>(for view: V) -> Content {
.view(
view,
visit: { [weak self] in
guard case let .view(view, _) = self?.content else { return }
// swiftlint:disable:next force_cast
$0.visit(view as! V)
}
)
}
}

View File

@ -1,46 +0,0 @@
// Copyright 2022 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 5/30/22.
//
extension FiberReconciler.Fiber: CustomDebugStringConvertible {
public var debugDescription: String {
let memoryAddress = String(format: "%010p", unsafeBitCast(self, to: Int.self))
if case let .view(view, _) = content,
let text = view as? Text
{
return "Text(\"\(text.storage.rawText)\") (\(memoryAddress))"
}
return "\(typeInfo?.name ?? "Unknown") (\(memoryAddress))"
}
private func flush(level: Int = 0) -> String {
let spaces = String(repeating: " ", count: level)
let geometry = geometry ?? .init(
origin: .init(origin: .zero),
dimensions: .init(size: .zero, alignmentGuides: [:]),
proposal: .unspecified
)
return """
\(spaces)\(String(describing: typeInfo?.type ?? Any.self)
.split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {\(element != nil ?
"\n\(spaces)geometry: \(geometry)" :
"")
\(child?.flush(level: level + 2) ?? "")
\(spaces)}
\(sibling?.flush(level: level) ?? "")
"""
}
}

View File

@ -1,626 +0,0 @@
// Copyright 2022 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 2/15/22.
//
import Foundation
import OpenCombineShim
// swiftlint:disable type_body_length
@_spi(TokamakCore)
public extension FiberReconciler {
/// A manager for a single `View`.
///
/// There are always 2 `Fiber`s for every `View` in the tree,
/// a current `Fiber`, and a work in progress `Fiber`.
/// They point to each other using the `alternate` property.
///
/// The current `Fiber` represents the `View` as it is currently rendered on the screen.
/// The work in progress `Fiber` (the `alternate` of current),
/// is used in the reconciler to compute the new tree.
///
/// When reconciling, the tree is recomputed from
/// the root of the state change on the work in progress `Fiber`.
/// Each node in the fiber tree is updated to apply any changes,
/// and a list of mutations needed to get the rendered output to match is created.
///
/// After the entire tree has been traversed, the current and work in progress trees are swapped,
/// making the updated tree the current one,
/// and leaving the previous current tree available to apply future changes on.
final class Fiber {
weak var reconciler: FiberReconciler<Renderer>?
/// The underlying value behind this `Fiber`. Either a `Scene` or `View` instance.
///
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance,
/// and captures a weak reference to `self` in the visitor function,
/// which requires all stored properties be set before capturing.
@_spi(TokamakCore)
public var content: Content!
/// Outputs from evaluating `View._makeView`
///
/// Stored as an IUO because creating `ViewOutputs` depends on
/// the `bindProperties` method, which requires
/// all stored properties be set before using.
/// `outputs` is guaranteed to be set in the initializer.
var outputs: ViewOutputs!
/// The erased `Layout` to use for this content.
///
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance.
var layout: AnyLayout?
/// The identity of this `View`
var id: Identity?
/// The mounted element, if this is a `Renderer` primitive.
var element: Renderer.ElementType?
/// The index of this element in its `elementParent`
var elementIndex: Int?
/// The first child node.
@_spi(TokamakCore)
public var child: Fiber?
/// This node's right sibling.
@_spi(TokamakCore)
public var sibling: Fiber?
/// An unowned reference to the parent node.
///
/// Parent references are `unowned` (as opposed to `weak`)
/// because the parent will always exist if a child does.
/// If the parent is released, the child is released with it.
@_spi(TokamakCore)
public unowned var parent: Fiber?
/// The nearest parent that can be mounted on.
unowned var elementParent: Fiber?
/// The nearest parent that receives preferences.
unowned var preferenceParent: Fiber?
/// The cached type information for the underlying `View`.
var typeInfo: TypeInfo?
/// Boxes that store `State` data.
var state: [PropertyInfo: MutableStorage] = [:]
/// Subscribed `Cancellable`s keyed with the property contained the observable.
///
/// Each time properties are bound, a new subscription could be created.
/// When the subscription is overridden, the old cancellable is released.
var subscriptions: [PropertyInfo: AnyCancellable] = [:]
/// Storage for `PreferenceKey` values as they are passed up the tree.
var preferences: _PreferenceStore?
/// The computed dimensions and origin.
var geometry: ViewGeometry?
/// The WIP node if this is current, or the current node if this is WIP.
@_spi(TokamakCore)
public weak var alternate: Fiber?
var createAndBindAlternate: (() -> Fiber?)?
/// A box holding a value for an `@State` property wrapper.
/// Will call `onSet` (usually a `Reconciler.reconcile` call) when updated.
final class MutableStorage {
private(set) var value: Any
let onSet: () -> ()
func setValue(_ newValue: Any, with transaction: Transaction) {
value = newValue
onSet()
}
init(initialValue: Any, onSet: @escaping () -> ()) {
value = initialValue
self.onSet = onSet
}
}
public enum Identity: Hashable {
case explicit(AnyHashable)
case structural(index: Int)
}
init<V: View>(
_ view: inout V,
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
elementIndex: Int?,
traits: _ViewTraitStore?,
reconciler: FiberReconciler<Renderer>?
) {
self.reconciler = reconciler
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
typeInfo = TokamakCore.typeInfo(of: V.self)
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &view, typeInfo, environment.environment)
var updateView = view
let viewInputs = ViewInputs(
content: view,
updateContent: { $0(&updateView) },
environment: environment,
traits: traits,
preferenceStore: preferences
)
outputs = V._makeView(viewInputs)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
view = updateView
content = content(for: view)
if let element = element {
self.element = element
} else if Renderer.isPrimitive(view) {
self.element = .init(
from: .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
)
}
if self.element != nil {
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
// Only specify an `elementIndex` if we have an element.
if self.element != nil {
self.elementIndex = elementIndex
}
let alternateView = view
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateView,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
parent: self.parent?.alternate,
elementParent: self.elementParent?.alternate,
preferenceParent: self.preferenceParent?.alternate,
reconciler: reconciler
)
self.alternate = alternate
if self.parent?.child === self {
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
} else {
// Find our left sibling.
var node = self.parent?.child
while node?.sibling !== self {
guard node?.sibling != nil else { return alternate }
node = node?.sibling
}
if node?.sibling === self {
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
}
}
return alternate
}
}
init<V: View>(
bound view: V,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout!,
alternate: Fiber,
outputs: ViewOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
parent: FiberReconciler<Renderer>.Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
if element != nil {
self.layout = layout
}
content = content(for: view)
}
private func bindProperties<T>(
to content: inout T,
_ typeInfo: TypeInfo?,
_ environment: EnvironmentValues
) {
var erased: Any = content
bindProperties(to: &erased, typeInfo, environment)
// swiftlint:disable:next force_cast
content = erased as! T
}
/// Collect `DynamicProperty`s and link their state changes to the reconciler.
private func bindProperties(
to content: inout Any,
_ typeInfo: TypeInfo?,
_ environment: EnvironmentValues
) {
guard let typeInfo = typeInfo else { return }
for property in typeInfo.properties where property.type is DynamicProperty.Type {
var value = property.get(from: content)
// Bind nested properties.
bindProperties(to: &value, TokamakCore.typeInfo(of: property.type), environment)
// Create boxes for `@State` and other mutable properties.
if var storage = value as? WritableValueStorage {
let box = self.state[property] ?? MutableStorage(
initialValue: storage.anyInitialValue,
onSet: { [weak self] in
guard let self = self else { return }
self.reconciler?.fiberChanged(self)
}
)
state[property] = box
storage.getter = { box.value }
storage.setter = { box.setValue($0, with: $1) }
value = storage
// Create boxes for `@StateObject` and other immutable properties.
} else if var storage = value as? ValueStorage {
let box = self.state[property] ?? MutableStorage(
initialValue: storage.anyInitialValue,
onSet: {}
)
state[property] = box
storage.getter = { box.value }
value = storage
// Read from the environment.
} else if var environmentReader = value as? EnvironmentReader {
environmentReader.setContent(from: environment)
value = environmentReader
}
// Subscribe to observable properties.
if let observed = value as? ObservedProperty {
subscriptions[property] = observed.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.reconciler?.fiberChanged(self)
}
}
property.set(value: value, on: &content)
}
if var environmentReader = content as? EnvironmentReader {
environmentReader.setContent(from: environment)
content = environmentReader
}
}
/// Call `update()` on each `DynamicProperty` in the type.
private func updateDynamicProperties(
of content: inout Any,
_ typeInfo: TypeInfo?
) {
guard let typeInfo = typeInfo else { return }
for property in typeInfo.properties where property.type is DynamicProperty.Type {
var value = property.get(from: content)
// Update nested properties.
updateDynamicProperties(of: &value, TokamakCore.typeInfo(of: property.type))
// swiftlint:disable:next force_cast
var dynamicProperty = value as! DynamicProperty
dynamicProperty.update()
property.set(value: dynamicProperty, on: &content)
}
}
/// Update each `DynamicProperty` in our content.
func updateDynamicProperties() {
guard let content = content else { return }
switch content {
case .app(var app, let visit):
updateDynamicProperties(of: &app, typeInfo)
self.content = .app(app, visit: visit)
case .scene(var scene, let visit):
updateDynamicProperties(of: &scene, typeInfo)
self.content = .scene(scene, visit: visit)
case .view(var view, let visit):
updateDynamicProperties(of: &view, typeInfo)
self.content = .view(view, visit: visit)
}
}
func update<V: View>(
with view: inout V,
elementIndex: Int?,
traits: _ViewTraitStore?
) -> Renderer.ElementType.Content? {
typeInfo = TokamakCore.typeInfo(of: V.self)
self.elementIndex = elementIndex
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &view, typeInfo, environment.environment)
var updateView = view
let inputs = ViewInputs(
content: view,
updateContent: {
$0(&updateView)
},
environment: environment,
traits: traits,
preferenceStore: preferences
)
outputs = V._makeView(inputs)
view = updateView
content = content(for: view)
if element != nil {
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
if Renderer.isPrimitive(view) {
return .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
} else {
return nil
}
}
init<A: App>(
_ app: inout A,
rootElement: Renderer.ElementType,
rootEnvironment: EnvironmentValues,
reconciler: FiberReconciler<Renderer>
) {
self.reconciler = reconciler
child = nil
sibling = nil
// `App`s are always the root, so they can have no parent.
parent = nil
elementParent = nil
preferenceParent = nil
element = rootElement
typeInfo = TokamakCore.typeInfo(of: A.self)
bindProperties(to: &app, typeInfo, rootEnvironment)
var updateApp = app
outputs = .init(
inputs: .init(
content: app,
updateContent: {
$0(&updateApp)
},
environment: .init(rootEnvironment),
traits: .init(),
preferenceStore: preferences
)
)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
app = updateApp
content = content(for: app)
layout = .init(RootLayout(renderer: reconciler.renderer))
let alternateApp = app
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateApp,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
reconciler: reconciler
)
self.alternate = alternate
return alternate
}
}
init<A: App>(
bound app: A,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout?,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
parent = nil
elementParent = nil
preferenceParent = nil
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
self.layout = layout
content = content(for: app)
}
init<S: Scene>(
_ scene: inout S,
element: Renderer.ElementType?,
parent: Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
environment: EnvironmentBox?,
reconciler: FiberReconciler<Renderer>?
) {
self.reconciler = reconciler
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.element = element
self.preferenceParent = preferenceParent
typeInfo = TokamakCore.typeInfo(of: S.self)
let environment = environment ?? parent?.outputs.environment ?? .init(.init())
bindProperties(to: &scene, typeInfo, environment.environment)
var updateScene = scene
outputs = S._makeScene(
.init(
content: scene,
updateContent: {
$0(&updateScene)
},
environment: environment,
traits: .init(),
preferenceStore: preferences
)
)
if let preferenceStore = outputs.preferenceStore {
preferences = preferenceStore
}
scene = updateScene
content = content(for: scene)
if element != nil {
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
let alternateScene = scene
createAndBindAlternate = { [weak self] in
guard let self = self else { return nil }
// Create the alternate lazily
let alternate = Fiber(
bound: alternateScene,
state: self.state,
subscriptions: self.subscriptions,
preferences: self.preferences,
layout: self.layout,
alternate: self,
outputs: self.outputs,
typeInfo: self.typeInfo,
element: self.element,
parent: self.parent?.alternate,
elementParent: self.elementParent?.alternate,
preferenceParent: self.preferenceParent?.alternate,
reconciler: reconciler
)
self.alternate = alternate
if self.parent?.child === self {
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
} else {
// Find our left sibling.
var node = self.parent?.child
while node?.sibling !== self {
guard node?.sibling != nil else { return alternate }
node = node?.sibling
}
if node?.sibling === self {
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
}
}
return alternate
}
}
init<S: Scene>(
bound scene: S,
state: [PropertyInfo: MutableStorage],
subscriptions: [PropertyInfo: AnyCancellable],
preferences: _PreferenceStore?,
layout: AnyLayout!,
alternate: Fiber,
outputs: SceneOutputs,
typeInfo: TypeInfo?,
element: Renderer.ElementType?,
parent: FiberReconciler<Renderer>.Fiber?,
elementParent: Fiber?,
preferenceParent: Fiber?,
reconciler: FiberReconciler<Renderer>?
) {
self.alternate = alternate
self.reconciler = reconciler
self.element = element
child = nil
sibling = nil
self.parent = parent
self.elementParent = elementParent
self.preferenceParent = preferenceParent
self.typeInfo = typeInfo
self.outputs = outputs
self.state = state
self.subscriptions = subscriptions
self.preferences = preferences
if element != nil {
self.layout = layout
}
content = content(for: scene)
}
func update<S: Scene>(
with scene: inout S
) -> Renderer.ElementType.Content? {
typeInfo = TokamakCore.typeInfo(of: S.self)
let environment = parent?.outputs.environment ?? .init(.init())
bindProperties(to: &scene, typeInfo, environment.environment)
var updateScene = scene
outputs = S._makeScene(.init(
content: scene,
updateContent: {
$0(&updateScene)
},
environment: environment,
traits: .init(),
preferenceStore: preferences
))
scene = updateScene
content = content(for: scene)
if element != nil {
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
}
return nil
}
}
}

View File

@ -1,33 +0,0 @@
// Copyright 2022 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 2/15/22.
//
/// A reference type that points to a `Renderer`-specific element that has been mounted.
/// For instance, a DOM node in the `DOMFiberRenderer`.
public protocol FiberElement: AnyObject {
associatedtype Content: FiberElementContent
var content: Content { get }
init(from content: Content)
func update(with content: Content)
}
/// The data used to create an `FiberElement`.
///
/// We re-use `FiberElement` instances in the `Fiber` tree,
/// but can re-create and copy `FiberElementContent` as often as needed.
public protocol FiberElementContent: Equatable {
init<V: View>(from primitiveView: V, useDynamicLayout: Bool)
}

View File

@ -1,212 +0,0 @@
// Copyright 2022 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 5/28/22.
//
import Foundation
extension FiberReconciler {
/// Convert the first level of children of a `View` into a linked list of `Fiber`s.
struct TreeReducer: SceneReducer {
final class Result {
// For references
let fiber: Fiber?
let visitChildren: (TreeReducer.SceneVisitor) -> ()
unowned var parent: Result?
var child: Result?
var sibling: Result?
var newContent: Renderer.ElementType.Content?
var elementIndices: [ObjectIdentifier: Int]
var nextTraits: _ViewTraitStore
// For reducing
var lastSibling: Result?
var nextExisting: Fiber?
var nextExistingAlternate: Fiber?
init(
fiber: Fiber?,
visitChildren: @escaping (TreeReducer.SceneVisitor) -> (),
parent: Result?,
child: Fiber?,
alternateChild: Fiber?,
newContent: Renderer.ElementType.Content? = nil,
elementIndices: [ObjectIdentifier: Int],
nextTraits: _ViewTraitStore
) {
self.fiber = fiber
self.visitChildren = visitChildren
self.parent = parent
nextExisting = child
nextExistingAlternate = alternateChild
self.newContent = newContent
self.elementIndices = elementIndices
self.nextTraits = nextTraits
}
}
static func reduce<S>(into partialResult: inout Result, nextScene: S) where S: Scene {
Self.reduce(
into: &partialResult,
nextValue: nextScene,
createFiber: { scene, element, parent, elementParent, preferenceParent, _, _, reconciler in
Fiber(
&scene,
element: element,
parent: parent,
elementParent: elementParent,
preferenceParent: preferenceParent,
environment: nil,
reconciler: reconciler
)
},
update: { fiber, scene, _, _ in
fiber.update(with: &scene)
},
visitChildren: { $1._visitChildren }
)
}
static func reduce<V>(into partialResult: inout Result, nextView: V) where V: View {
Self.reduce(
into: &partialResult,
nextValue: nextView,
createFiber: {
view, element,
parent, elementParent, preferenceParent, elementIndex,
traits, reconciler in
Fiber(
&view,
element: element,
parent: parent,
elementParent: elementParent,
preferenceParent: preferenceParent,
elementIndex: elementIndex,
traits: traits,
reconciler: reconciler
)
},
update: { fiber, view, elementIndex, traits in
fiber.update(
with: &view,
elementIndex: elementIndex,
traits: fiber.element != nil ? traits : nil
)
},
visitChildren: { reconciler, view in
reconciler?.renderer.viewVisitor(for: view) ?? view._visitChildren
}
)
}
static func reduce<T>(
into partialResult: inout Result,
nextValue: T,
createFiber: (
inout T,
Renderer.ElementType?,
Fiber?,
Fiber?,
Fiber?,
Int?,
_ViewTraitStore,
FiberReconciler?
) -> Fiber,
update: (Fiber, inout T, Int?, _ViewTraitStore) -> Renderer.ElementType.Content?,
visitChildren: (FiberReconciler?, T) -> (TreeReducer.SceneVisitor) -> ()
) {
// Create the node and its element.
var nextValue = nextValue
let resultChild: Result
if let existing = partialResult.nextExisting {
// If a fiber already exists, simply update it with the new view.
let key: ObjectIdentifier?
if let elementParent = existing.elementParent {
key = ObjectIdentifier(elementParent)
} else {
key = nil
}
let newContent = update(
existing,
&nextValue,
key.map { partialResult.elementIndices[$0, default: 0] },
partialResult.nextTraits
)
resultChild = Result(
fiber: existing,
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
parent: partialResult,
child: existing.child,
alternateChild: existing.alternate?.child,
newContent: newContent,
elementIndices: partialResult.elementIndices,
nextTraits: existing.element != nil ? .init() : partialResult.nextTraits
)
partialResult.nextExisting = existing.sibling
partialResult.nextExistingAlternate = partialResult.nextExistingAlternate?.sibling
} else {
let elementParent = partialResult.fiber?.element != nil
? partialResult.fiber
: partialResult.fiber?.elementParent
let preferenceParent = partialResult.fiber?.preferences != nil
? partialResult.fiber
: partialResult.fiber?.preferenceParent
let key: ObjectIdentifier?
if let elementParent = elementParent {
key = ObjectIdentifier(elementParent)
} else {
key = nil
}
// Otherwise, create a new fiber for this child.
let fiber = createFiber(
&nextValue,
partialResult.nextExistingAlternate?.element,
partialResult.fiber,
elementParent,
preferenceParent,
key.map { partialResult.elementIndices[$0, default: 0] },
partialResult.nextTraits,
partialResult.fiber?.reconciler
)
// If a fiber already exists for an alternate, link them.
if let alternate = partialResult.nextExistingAlternate {
fiber.alternate = alternate
partialResult.nextExistingAlternate = alternate.sibling
}
resultChild = Result(
fiber: fiber,
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
parent: partialResult,
child: nil,
alternateChild: fiber.alternate?.child,
elementIndices: partialResult.elementIndices,
nextTraits: fiber.element != nil ? .init() : partialResult.nextTraits
)
}
// Get the last child element we've processed, and add the new child as its sibling.
if let lastSibling = partialResult.lastSibling {
lastSibling.fiber?.sibling = resultChild.fiber
lastSibling.sibling = resultChild
} else {
// Otherwise setup the first child
partialResult.fiber?.child = resultChild.fiber
partialResult.child = resultChild
}
partialResult.lastSibling = resultChild
}
}
}

View File

@ -1,290 +0,0 @@
// Copyright 2022 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 2/15/22.
//
import Foundation
import OpenCombineShim
/// A reconciler modeled after React's
/// [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
public final class FiberReconciler<Renderer: FiberRenderer> {
/// The root node in the `Fiber` tree that represents the `View`s currently rendered on screen.
@_spi(TokamakCore)
public var current: Fiber!
/// The alternate of `current`, or the work in progress tree root.
///
/// We must keep a strong reference to both the current and alternate tree roots,
/// as they only keep weak references to each other.
private var alternate: Fiber!
/// The `FiberRenderer` used to create and update the `Element`s on screen.
public let renderer: Renderer
/// Enabled passes to run on each `reconcile(from:)` call.
private let passes: [FiberReconcilerPass]
private let caches: Caches
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
let reconciler: FiberReconciler<Renderer>
var environment: EnvironmentValues {
var environment = reconciler.renderer.defaultEnvironment
environment.measureText = reconciler.renderer.measureText
environment.measureImage = reconciler.renderer.measureImage
environment.afterReconcile = reconciler.afterReconcile
return environment
}
var body: some View {
RootLayout(renderer: reconciler.renderer).callAsFunction {
content
.environmentValues(environment)
}
}
}
/// The `Layout` container for the root of a `View` hierarchy.
///
/// Simply places each `View` in the center of its bounds.
struct RootLayout: Layout {
let renderer: Renderer
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
renderer.sceneSize.value
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
for subview in subviews {
subview.place(
at: .init(x: bounds.midX, y: bounds.midY),
anchor: .center,
proposal: .init(width: bounds.width, height: bounds.height)
)
}
}
}
public init<V: View>(_ renderer: Renderer, _ view: V) {
self.renderer = renderer
if renderer.useDynamicLayout {
passes = [.reconcile, .layout]
} else {
passes = [.reconcile]
}
caches = Caches()
var view = RootView(content: view, reconciler: self)
current = .init(
&view,
element: renderer.rootElement,
parent: nil,
elementParent: nil,
preferenceParent: nil,
elementIndex: 0,
traits: nil,
reconciler: self
)
// Start by building the initial tree.
alternate = current.createAndBindAlternate?()
sceneSizeCancellable = renderer.sceneSize.removeDuplicates().sink { [weak self] _ in
guard let self = self else { return }
self.fiberChanged(self.current)
}
}
public init<A: App>(_ renderer: Renderer, _ app: A) {
self.renderer = renderer
if renderer.useDynamicLayout {
passes = [.reconcile, .layout]
} else {
passes = [.reconcile]
}
caches = Caches()
var environment = renderer.defaultEnvironment
environment.measureText = renderer.measureText
environment.measureImage = renderer.measureImage
environment.afterReconcile = afterReconcile
var app = app
current = .init(
&app,
rootElement: renderer.rootElement,
rootEnvironment: environment,
reconciler: self
)
// Start by building the initial tree.
alternate = current.createAndBindAlternate?()
sceneSizeCancellable = renderer.sceneSize.removeDuplicates().sink { [weak self] _ in
guard let self = self else { return }
self.fiberChanged(self.current)
}
}
/// A visitor that performs each pass used by the `FiberReconciler`.
final class ReconcilerVisitor: AppVisitor, SceneVisitor, ViewVisitor {
let root: Fiber
/// Any `Fiber`s that changed state during the last run loop.
let changedFibers: Set<ObjectIdentifier>
unowned let reconciler: FiberReconciler
var mutations = [Mutation<Renderer>]()
init(root: Fiber, changedFibers: Set<ObjectIdentifier>, reconciler: FiberReconciler) {
self.root = root
self.changedFibers = changedFibers
self.reconciler = reconciler
}
func visit<A>(_ app: A) where A: App {
visitAny(app) { $0.visit(app.body) }
}
func visit<S>(_ scene: S) where S: Scene {
visitAny(scene, scene._visitChildren)
}
func visit<V>(_ view: V) where V: View {
visitAny(view, reconciler.renderer.viewVisitor(for: view))
}
private func visitAny(
_ content: Any,
_ visitChildren: @escaping (TreeReducer.SceneVisitor) -> ()
) {
let alternateRoot: Fiber?
if let alternate = root.alternate {
alternateRoot = alternate
} else {
alternateRoot = root.createAndBindAlternate?()
}
let rootResult = TreeReducer.Result(
fiber: alternateRoot, // The alternate is the WIP node.
visitChildren: visitChildren,
parent: nil,
child: alternateRoot?.child,
alternateChild: root.child,
elementIndices: [:],
nextTraits: .init()
)
reconciler.caches.clear()
for pass in reconciler.passes {
pass.run(
in: reconciler,
root: rootResult,
changedFibers: changedFibers,
caches: reconciler.caches
)
}
mutations = reconciler.caches.mutations
}
}
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.
func fiberChanged(_ fiber: Fiber) {
guard let alternate = fiber.alternate ?? fiber.createAndBindAlternate?()
else { return }
let shouldSchedule = changedFibers.isEmpty
changedFibers.insert(ObjectIdentifier(alternate))
if shouldSchedule {
renderer.schedule { [weak self] in
self?.reconcile()
}
}
}
/// Perform each `FiberReconcilerPass` given the `changedFibers`.
///
/// 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.
let visitor = ReconcilerVisitor(root: current, changedFibers: changedFibers, reconciler: self)
switch current.content {
case let .view(_, visit):
visit(visitor)
case let .scene(_, visit):
visit(visitor)
case let .app(_, visit):
visit(visitor)
case .none:
break
}
// Apply mutations to the rendered output.
renderer.commit(visitor.mutations)
// Swap the root out for its alternate.
// Essentially, making the work in progress tree the current,
// and leaving the current available to be the work in progress
// on our next update.
let alternate = alternate
self.alternate = current
current = alternate
isReconciling = false
for action in afterReconcileActions {
action()
}
}
}
public extension EnvironmentValues {
private enum AfterReconcileKey: EnvironmentKey {
static let defaultValue: (@escaping () -> ()) -> () = { _ in }
}
var afterReconcile: (@escaping () -> ()) -> () {
get { self[AfterReconcileKey.self] }
set { self[AfterReconcileKey.self] = newValue }
}
}

View File

@ -1,145 +0,0 @@
// Copyright 2022 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 2/15/22.
//
import Foundation
import OpenCombineShim
/// A renderer capable of performing mutations specified by a `FiberReconciler`.
public protocol FiberRenderer {
/// The element class this renderer uses.
associatedtype ElementType: FiberElement
/// Check whether a `View` is a primitive for this renderer.
static func isPrimitive<V>(_ view: V) -> Bool where V: View
/// Override the default `_visitChildren` implementation for a primitive `View`.
func visitPrimitiveChildren<Primitive, Visitor>(
_ view: Primitive
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor
/// Apply the mutations to the elements.
func commit(_ mutations: [Mutation<Self>])
/// The root element all top level views should be mounted on.
var rootElement: ElementType { get }
/// The smallest set of initial `EnvironmentValues` needed for this renderer to function.
var defaultEnvironment: EnvironmentValues { get }
/// The size of the window we are rendering in.
///
/// Layout is automatically updated whenever the size changes.
var sceneSize: CurrentValueSubject<CGSize, Never> { get }
/// Whether layout is enabled for this renderer.
var useDynamicLayout: Bool { get }
/// Calculate the size of `Text` in `environment` for layout.
func measureText(
_ text: Text,
proposal: ProposedViewSize,
in environment: EnvironmentValues
) -> CGSize
/// Calculate the size of an `Image` in `environment` for layout.
func measureImage(
_ image: Image,
proposal: ProposedViewSize,
in environment: EnvironmentValues
) -> CGSize
/// Run `action` on the next run loop.
///
/// Called by the `FiberReconciler` to perform reconciliation after all changed Fibers are collected.
///
/// For example, take the following sample `View`:
///
/// struct DuelOfTheStates: View {
/// @State private var hits1 = 0
/// @State private var hits2 = 0
///
/// var body: some View {
/// Button("Hit") {
/// hits1 += 1
/// hits2 += 2
/// }
/// }
/// }
///
/// When the button is pressed, both `hits1` and `hits2` are updated.
/// If reconciliation was done on every state change, we would needlessly run it twice,
/// once for `hits1` and again for `hits2`.
///
/// Instead, we create a list of changed fibers
/// (in this case just `DuelOfTheStates` as both properties were on it),
/// and reconcile after all changes have been collected.
func schedule(_ action: @escaping () -> ())
}
public extension FiberRenderer {
var defaultEnvironment: EnvironmentValues { .init() }
func visitPrimitiveChildren<Primitive, Visitor>(
_ view: Primitive
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor {
nil
}
func viewVisitor<V: View, Visitor: ViewVisitor>(for view: V) -> ViewVisitorF<Visitor> {
if Self.isPrimitive(view) {
return visitPrimitiveChildren(view) ?? view._visitChildren
} else {
return view._visitChildren
}
}
@discardableResult
@_disfavoredOverload
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
.init(self, view)
}
@discardableResult
@_disfavoredOverload
func render<A: App>(_ app: A) -> FiberReconciler<Self> {
.init(self, app)
}
}
extension EnvironmentValues {
private enum MeasureTextKey: EnvironmentKey {
static var defaultValue: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
{ _, _, _ in .zero }
}
}
var measureText: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
get { self[MeasureTextKey.self] }
set { self[MeasureTextKey.self] = newValue }
}
private enum MeasureImageKey: EnvironmentKey {
static var defaultValue: (Image, ProposedViewSize, EnvironmentValues) -> CGSize {
{ _, _, _ in .zero }
}
}
var measureImage: (Image, ProposedViewSize, EnvironmentValues) -> CGSize {
get { self[MeasureImageKey.self] }
set { self[MeasureImageKey.self] = newValue }
}
}

View File

@ -1,136 +0,0 @@
// Copyright 2022 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 6/20/22.
//
import Foundation
/// The cache for a `ContainedZLayout`.
@_spi(TokamakCore)
public struct ContainedZLayoutCache {
/// The result of `dimensions(in:)` for the primary subview.
var primaryDimensions: ViewDimensions?
}
/// A layout that fits secondary subviews to the size of a primary subview.
///
/// Used to implement `_BackgroundLayout` and `_OverlayLayout`.
@_spi(TokamakCore)
public protocol ContainedZLayout: Layout where Cache == ContainedZLayoutCache {
var alignment: Alignment { get }
/// An accessor for the primary subview from a `LayoutSubviews` collection.
static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { get }
}
@_spi(TokamakCore)
public extension ContainedZLayout {
func makeCache(subviews: Subviews) -> Cache {
.init()
}
func spacing(subviews: LayoutSubviews, cache: inout Cache) -> ViewSpacing {
subviews[keyPath: Self.primarySubview]?.spacing ?? .init()
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
// Assume the dimensions of the primary subview.
cache.primaryDimensions = subviews[keyPath: Self.primarySubview]?.dimensions(in: proposal)
return .init(
width: cache.primaryDimensions?.width ?? .zero,
height: cache.primaryDimensions?.height ?? .zero
)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
let proposal = ProposedViewSize(bounds.size)
// Place the foreground at the origin.
subviews[keyPath: Self.primarySubview]?.place(at: bounds.origin, proposal: proposal)
let backgroundSubviews = subviews[keyPath: Self.primarySubview] == subviews.first
? subviews.dropFirst(1)
: subviews.dropLast(1)
/// The `ViewDimensions` of the subview with the greatest `width`, used to follow `alignment`.
var widest: ViewDimensions?
/// The `ViewDimensions` of the subview with the greatest `height`.
var tallest: ViewDimensions?
let dimensions = backgroundSubviews.map { subview -> ViewDimensions in
let dimensions = subview.dimensions(in: proposal)
if dimensions.width > (widest?.width ?? .zero) {
widest = dimensions
}
if dimensions.height > (tallest?.height ?? .zero) {
tallest = dimensions
}
return dimensions
}
/// The alignment guide values of the primary subview.
let primaryOffset = CGSize(
width: cache.primaryDimensions?[alignment.horizontal] ?? .zero,
height: cache.primaryDimensions?[alignment.vertical] ?? .zero
)
/// The alignment guide values of the secondary subviews (background/overlay).
/// Uses the widest/tallest element to get the full extents.
let secondaryOffset = CGSize(
width: widest?[alignment.horizontal] ?? .zero,
height: tallest?[alignment.vertical] ?? .zero
)
/// The center offset of the secondary subviews.
let secondaryCenter = CGSize(
width: widest?[HorizontalAlignment.center] ?? .zero,
height: tallest?[VerticalAlignment.center] ?? .zero
)
/// The origin of the secondary subviews with alignment.
let secondaryOrigin = CGPoint(
x: bounds.minX + primaryOffset.width - secondaryOffset.width + secondaryCenter.width,
y: bounds.minY + primaryOffset.height - secondaryOffset.height + secondaryCenter.height
)
for (index, subview) in backgroundSubviews.enumerated() {
// Background elements are centered between each other, but placed with `alignment`
// all together on the foreground.
subview.place(
at: .init(
x: secondaryOrigin.x - dimensions[index][HorizontalAlignment.center],
y: secondaryOrigin.y - dimensions[index][VerticalAlignment.center]
),
proposal: proposal
)
}
}
}
/// Expects the primary subview to be last.
@_spi(TokamakCore)
extension _BackgroundLayout: ContainedZLayout {
public static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { \.last }
}
/// Expects the primary subview to be the first.
@_spi(TokamakCore)
extension _OverlayLayout: ContainedZLayout {
public static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { \.first }
}

View File

@ -1,477 +0,0 @@
// Copyright 2022 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 2/16/22.
//
import Foundation
/// Erase a `Layout` conformance to an `AnyLayout`.
///
/// This could potentially be removed in Swift 5.7 in favor of `any Layout`.
public protocol _AnyLayout {
func _erased() -> AnyLayout
}
/// A type that participates in the layout pass.
///
/// Any `View` or `Scene` that implements this protocol will be used to compute layout in
/// a `FiberRenderer` with `useDynamicLayout` set to `true`.
public protocol Layout: Animatable, _AnyLayout {
static var layoutProperties: LayoutProperties { get }
associatedtype Cache = ()
/// Proxies for the children of this container.
typealias Subviews = LayoutSubviews
/// Create a fresh `Cache`. Use it to store complex operations,
/// or to pass data between `sizeThatFits` and `placeSubviews`.
///
/// - Note: There are no guarantees about when the cache will be recreated,
/// and the behavior could change at any time.
func makeCache(subviews: Self.Subviews) -> Self.Cache
/// Update the existing `Cache` before each layout pass.
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)
/// The preferred spacing for this `View` and its subviews.
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing
/// Request a size to contain the subviews and fit within `proposal`.
/// If you provide a size that does not fit within `proposal`, the parent will still respect it.
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGSize
/// Place each subview with `LayoutSubview.place(at:anchor:proposal:)`.
///
/// - Note: The bounds are not necessarily at `(0, 0)`, so use `bounds.minX` and `bounds.minY`
/// to correctly position relative to the container.
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
)
/// Override the value of a `HorizontalAlignment` value.
func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat?
/// Override the value of a `VerticalAlignment` value.
func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat?
}
public extension Layout {
func _erased() -> AnyLayout {
.init(self)
}
}
public extension Layout where Self.Cache == () {
func makeCache(subviews: Self.Subviews) -> Self.Cache {
()
}
}
public extension Layout {
static var layoutProperties: LayoutProperties {
.init()
}
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews) {
cache = makeCache(subviews: subviews)
}
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing {
subviews.reduce(
into: subviews.first.map {
.init(
viewType: $0.spacing.viewType,
top: { _ in 0 },
leading: { _ in 0 },
bottom: { _ in 0 },
trailing: { _ in 0 }
)
} ?? .zero
) { $0.formUnion($1.spacing) }
}
func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat? {
nil
}
func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat? {
nil
}
}
public extension Layout {
/// Render `content` using `self` as the layout container.
func callAsFunction<V>(@ViewBuilder _ content: () -> V) -> some View where V: View {
LayoutView(layout: self, content: content())
}
}
/// A `View` that renders its children with a `Layout`.
@_spi(TokamakCore)
public struct LayoutView<L: Layout, Content: View>: View, Layout {
let layout: L
let content: Content
public typealias Cache = L.Cache
public func makeCache(subviews: Subviews) -> L.Cache {
layout.makeCache(subviews: subviews)
}
public func updateCache(_ cache: inout L.Cache, subviews: Subviews) {
layout.updateCache(&cache, subviews: subviews)
}
public func spacing(subviews: Subviews, cache: inout L.Cache) -> ViewSpacing {
layout.spacing(subviews: subviews, cache: &cache)
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache)
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
layout.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &cache)
}
public func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout L.Cache
) -> CGFloat? {
layout.explicitAlignment(
of: guide, in: bounds, proposal: proposal, subviews: subviews, cache: &cache
)
}
public func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout L.Cache
) -> CGFloat? {
layout.explicitAlignment(
of: guide, in: bounds, proposal: proposal, subviews: subviews, cache: &cache
)
}
public var body: some View {
content
}
}
/// A default `Layout` that fits to the first subview and places its children at its origin.
struct DefaultLayout: Layout {
/// An erased `DefaultLayout` that is shared between all views.
static let shared: AnyLayout = .init(Self())
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let size = subviews.first?.sizeThatFits(proposal) ?? .zero
return size
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
for subview in subviews {
subview.place(at: bounds.origin, proposal: proposal)
}
}
}
/// Describes a container for an erased `Layout` type.
///
/// Matches the `Layout` protocol with `Cache` erased to `Any`.
@usableFromInline
protocol AnyLayoutBox: AnyObject {
var layoutProperties: LayoutProperties { get }
typealias Subviews = LayoutSubviews
typealias Cache = Any
func makeCache(subviews: Self.Subviews) -> Self.Cache
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGSize
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
)
func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat?
func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Self.Subviews,
cache: inout Self.Cache
) -> CGFloat?
var animatableData: _AnyAnimatableData { get set }
}
final class ConcreteLayoutBox<L: Layout>: AnyLayoutBox {
var base: L
init(_ base: L) {
self.base = base
}
var layoutProperties: LayoutProperties { L.layoutProperties }
func makeCache(subviews: Subviews) -> Cache {
base.makeCache(subviews: subviews)
}
private func typedCache<R>(
subviews: Subviews,
erasedCache: inout Cache,
_ action: (inout L.Cache) -> R
) -> R {
var typedCache = erasedCache as? L.Cache ?? base.makeCache(subviews: subviews)
defer { erasedCache = typedCache }
return action(&typedCache)
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
typedCache(subviews: subviews, erasedCache: &cache) {
base.updateCache(&$0, subviews: subviews)
}
}
func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing {
typedCache(subviews: subviews, erasedCache: &cache) {
base.spacing(subviews: subviews, cache: &$0)
}
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
typedCache(subviews: subviews, erasedCache: &cache) {
base.sizeThatFits(proposal: proposal, subviews: subviews, cache: &$0)
}
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
typedCache(subviews: subviews, erasedCache: &cache) {
base.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &$0)
}
}
func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGFloat? {
typedCache(subviews: subviews, erasedCache: &cache) {
base.explicitAlignment(
of: guide,
in: bounds,
proposal: proposal,
subviews: subviews,
cache: &$0
)
}
}
func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGFloat? {
typedCache(subviews: subviews, erasedCache: &cache) {
base.explicitAlignment(
of: guide,
in: bounds,
proposal: proposal,
subviews: subviews,
cache: &$0
)
}
}
var animatableData: _AnyAnimatableData {
get {
.init(base.animatableData)
}
set {
guard let newData = newValue.value as? L.AnimatableData else { return }
base.animatableData = newData
}
}
}
@frozen
public struct AnyLayout: Layout {
var storage: AnyLayoutBox
public init<L>(_ layout: L) where L: Layout {
storage = ConcreteLayoutBox(layout)
}
public struct Cache {
var erasedCache: Any
}
public func makeCache(subviews: AnyLayout.Subviews) -> AnyLayout.Cache {
.init(erasedCache: storage.makeCache(subviews: subviews))
}
public func updateCache(_ cache: inout AnyLayout.Cache, subviews: AnyLayout.Subviews) {
storage.updateCache(&cache.erasedCache, subviews: subviews)
}
public func spacing(subviews: AnyLayout.Subviews, cache: inout AnyLayout.Cache) -> ViewSpacing {
storage.spacing(subviews: subviews, cache: &cache.erasedCache)
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: AnyLayout.Subviews,
cache: inout AnyLayout.Cache
) -> CGSize {
storage.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache.erasedCache)
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: AnyLayout.Subviews,
cache: inout AnyLayout.Cache
) {
storage.placeSubviews(
in: bounds,
proposal: proposal,
subviews: subviews,
cache: &cache.erasedCache
)
}
public func explicitAlignment(
of guide: HorizontalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: AnyLayout.Subviews,
cache: inout AnyLayout.Cache
) -> CGFloat? {
storage.explicitAlignment(
of: guide,
in: bounds,
proposal: proposal,
subviews: subviews,
cache: &cache.erasedCache
)
}
public func explicitAlignment(
of guide: VerticalAlignment,
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: AnyLayout.Subviews,
cache: inout AnyLayout.Cache
) -> CGFloat? {
storage.explicitAlignment(
of: guide, in: bounds,
proposal: proposal,
subviews: subviews,
cache: &cache.erasedCache
)
}
public var animatableData: _AnyAnimatableData {
get {
_AnyAnimatableData(storage.animatableData)
}
set {
storage.animatableData = newValue
}
}
}

View File

@ -1,31 +0,0 @@
// Copyright 2022 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 6/22/22.
//
import Foundation
@usableFromInline
enum LayoutPriorityTraitKey: _ViewTraitKey {
@inlinable
static var defaultValue: Double { 0 }
}
public extension View {
@inlinable
func layoutPriority(_ value: Double) -> some View {
_trait(LayoutPriorityTraitKey.self, value)
}
}

View File

@ -1,25 +0,0 @@
// Copyright 2022 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 6/20/22.
//
/// Metadata about a `Layout`.
public struct LayoutProperties {
public var stackOrientation: Axis?
public init() {
stackOrientation = nil
}
}

View File

@ -1,252 +0,0 @@
// Copyright 2022 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 6/20/22.
//
import Foundation
/// A collection of `LayoutSubview` proxies.
public struct LayoutSubviews: Equatable, RandomAccessCollection {
public var layoutDirection: LayoutDirection
var storage: [LayoutSubview]
init(layoutDirection: LayoutDirection, storage: [LayoutSubview]) {
self.layoutDirection = layoutDirection
self.storage = storage
}
init<R: FiberRenderer>(_ node: FiberReconciler<R>.Fiber) {
self.init(
layoutDirection: node.outputs.environment.environment.layoutDirection,
storage: []
)
}
public typealias SubSequence = LayoutSubviews
public typealias Element = LayoutSubview
public typealias Index = Int
public typealias Indices = Range<LayoutSubviews.Index>
public typealias Iterator = IndexingIterator<LayoutSubviews>
public var startIndex: Int {
storage.startIndex
}
public var endIndex: Int {
storage.endIndex
}
public subscript(index: Int) -> LayoutSubviews.Element {
storage[index]
}
public subscript(bounds: Range<Int>) -> LayoutSubviews {
.init(layoutDirection: layoutDirection, storage: .init(storage[bounds]))
}
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)
)
}
}
/// A proxy representing a child of a `Layout`.
///
/// Access size requests, alignment guide values, spacing preferences, and any layout values using
/// this proxy.
///
/// `Layout` types are expected to call `place(at:anchor:proposal:)` on all subviews.
/// If `place(at:anchor:proposal:)` is not called, the center will be used as its position.
public struct LayoutSubview: Equatable {
private let id: ObjectIdentifier
private let storage: AnyStorage
/// A protocol used to erase `Storage<R>`.
private class AnyStorage {
let traits: _ViewTraitStore?
init(traits: _ViewTraitStore?) {
self.traits = traits
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
fatalError("Implement \(#function) in subclass")
}
func dimensions(_ sizeThatFits: CGSize) -> ViewDimensions {
fatalError("Implement \(#function) in subclass")
}
func place(
_ proposal: ProposedViewSize,
_ dimensions: ViewDimensions,
_ position: CGPoint,
_ anchor: UnitPoint
) {
fatalError("Implement \(#function) in subclass")
}
func spacing() -> ViewSpacing {
fatalError("Implement \(#function) in subclass")
}
}
/// The backing storage for a `LayoutSubview`. This contains the underlying implementations for
/// methods accessing the `fiber`, `element`, and `cache` this subview represents.
private final class Storage<R: FiberRenderer>: AnyStorage {
weak var fiber: FiberReconciler<R>.Fiber?
weak var element: R.ElementType?
unowned var caches: FiberReconciler<R>.Caches
init(
traits: _ViewTraitStore?,
fiber: FiberReconciler<R>.Fiber?,
element: R.ElementType?,
caches: FiberReconciler<R>.Caches
) {
self.fiber = fiber
self.element = element
self.caches = caches
super.init(traits: traits)
}
override func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
guard let fiber = fiber else { return .zero }
let request = FiberReconciler<R>.Caches.LayoutCache.SizeThatFitsRequest(proposal)
return caches.updateLayoutCache(for: fiber) { cache in
guard let layout = fiber.layout else { return .zero }
if let size = cache.sizeThatFits[request] {
return size
} else {
let size = layout.sizeThatFits(
proposal: proposal,
subviews: caches.layoutSubviews(for: fiber),
cache: &cache.cache
)
cache.sizeThatFits[request] = size
if let alternate = fiber.alternate {
caches.updateLayoutCache(for: alternate) { alternateCache in
alternateCache.cache = cache.cache
alternateCache.sizeThatFits[request] = size
}
}
return size
}
} ?? .zero
}
override func dimensions(_ sizeThatFits: CGSize) -> ViewDimensions {
// TODO: Add `alignmentGuide` modifier and pass into `ViewDimensions`
ViewDimensions(size: sizeThatFits, alignmentGuides: [:])
}
override func place(
_ proposal: ProposedViewSize,
_ dimensions: ViewDimensions,
_ position: CGPoint,
_ anchor: UnitPoint
) {
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)
)),
dimensions: dimensions,
proposal: proposal
)
// Push a layout mutation if needed.
if geometry != fiber.alternate?.geometry {
caches.mutations.append(.layout(element: element, geometry: geometry))
}
// Update ours and our alternate's geometry
fiber.geometry = geometry
fiber.alternate?.geometry = geometry
}
override func spacing() -> ViewSpacing {
guard let fiber = fiber else { return .init() }
return caches.updateLayoutCache(for: fiber) { cache in
fiber.layout?.spacing(
subviews: caches.layoutSubviews(for: fiber),
cache: &cache.cache
) ?? .zero
} ?? .zero
}
}
init<R: FiberRenderer>(
id: ObjectIdentifier,
traits: _ViewTraitStore?,
fiber: FiberReconciler<R>.Fiber,
element: R.ElementType,
caches: FiberReconciler<R>.Caches
) {
self.id = id
storage = Storage(
traits: traits,
fiber: fiber,
element: element,
caches: caches
)
}
public func _trait<K>(key: K.Type) -> K.Value where K: _ViewTraitKey {
storage.traits?.value(forKey: key) ?? K.defaultValue
}
public subscript<K>(key: K.Type) -> K.Value where K: LayoutValueKey {
_trait(key: _LayoutTrait<K>.self)
}
public var priority: Double {
_trait(key: LayoutPriorityTraitKey.self)
}
public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
storage.sizeThatFits(proposal)
}
public func dimensions(in proposal: ProposedViewSize) -> ViewDimensions {
storage.dimensions(sizeThatFits(proposal))
}
public var spacing: ViewSpacing {
storage.spacing()
}
public func place(
at position: CGPoint,
anchor: UnitPoint = .topLeading,
proposal: ProposedViewSize
) {
storage.place(
proposal,
dimensions(in: proposal),
position,
anchor
)
}
public static func == (lhs: LayoutSubview, rhs: LayoutSubview) -> Bool {
lhs.storage === rhs.storage
}
}

View File

@ -1,36 +0,0 @@
// Copyright 2022 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 6/20/22.
//
/// A key that stores a value that can be accessed via a `LayoutSubview`.
public protocol LayoutValueKey {
associatedtype Value
static var defaultValue: Self.Value { get }
}
public extension View {
@inlinable
func layoutValue<K>(key: K.Type, value: K.Value) -> some View where K: LayoutValueKey {
// LayoutValueKey uses trait keys under the hood.
_trait(_LayoutTrait<K>.self, value)
}
}
public struct _LayoutTrait<K>: _ViewTraitKey where K: LayoutValueKey {
public static var defaultValue: K.Value {
K.defaultValue
}
}

View File

@ -1,84 +0,0 @@
// Copyright 2022 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 5/28/22.
//
import Foundation
private extension EdgeInsets {
init(applying edges: Edge.Set, to insets: EdgeInsets) {
self.init(
top: edges.contains(.top) ? insets.top : 0,
leading: edges.contains(.leading) ? insets.leading : 0,
bottom: edges.contains(.bottom) ? insets.bottom : 0,
trailing: edges.contains(.trailing) ? insets.trailing : 0
)
}
}
private struct PaddingLayout: Layout {
let edges: Edge.Set
let insets: EdgeInsets?
func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
.init()
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
let proposal = proposal.replacingUnspecifiedDimensions()
let insets = EdgeInsets(applying: edges, to: insets ?? .init(_all: 10))
let subviewSize = subviews.first?.sizeThatFits(
.init(
width: proposal.width - insets.leading - insets.trailing,
height: proposal.height - insets.top - insets.bottom
)
) ?? .zero
return .init(
width: subviewSize.width + insets.leading + insets.trailing,
height: subviewSize.height + insets.top + insets.bottom
)
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let insets = EdgeInsets(applying: edges, to: insets ?? .init(_all: 10))
let proposal = proposal.replacingUnspecifiedDimensions()
for subview in subviews {
subview.place(
at: .init(x: bounds.minX + insets.leading, y: bounds.minY + insets.top),
proposal: .init(
width: proposal.width - insets.leading - insets.trailing,
height: proposal.height - insets.top - insets.bottom
)
)
}
}
}
public extension _PaddingLayout {
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
visitor.visit(PaddingLayout(edges: edges, insets: insets).callAsFunction {
content
})
}
}

View File

@ -1,44 +0,0 @@
// Copyright 2022 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 6/20/22.
//
import Foundation
@frozen
public struct ProposedViewSize: Equatable {
public var width: CGFloat?
public var height: CGFloat?
public static let zero: ProposedViewSize = .init(width: 0, height: 0)
public static let unspecified: ProposedViewSize = .init(width: nil, height: nil)
public static let infinity: ProposedViewSize = .init(width: .infinity, height: .infinity)
@inlinable
public init(width: CGFloat?, height: CGFloat?) {
(self.width, self.height) = (width, height)
}
@inlinable
public init(_ size: CGSize) {
self.init(width: size.width, height: size.height)
}
@inlinable
public func replacingUnspecifiedDimensions(by size: CGSize = CGSize(
width: 10,
height: 10
)) -> CGSize {
CGSize(width: width ?? size.width, height: height ?? size.height)
}
}

View File

@ -1,268 +0,0 @@
// Copyright 2022 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 5/24/22.
//
import Foundation
private extension ViewDimensions {
/// Access the guide value of an `Alignment` for a particular `Axis`.
subscript(alignment alignment: Alignment, in axis: Axis) -> CGFloat {
switch axis {
case .horizontal: return self[alignment.vertical]
case .vertical: return self[alignment.horizontal]
}
}
}
/// The `Layout.Cache` for `StackLayout` conforming types.
@_spi(TokamakCore)
public struct StackLayoutCache {
/// The widest/tallest (depending on the `axis`) subview.
/// Used to place subviews along the `alignment`.
var maxSubview: ViewDimensions?
/// The ideal size for each subview as computed in `sizeThatFits`.
var idealSizes = [CGSize]()
}
/// An internal structure used to store layout information about
/// `LayoutSubview`s of a `StackLayout` that will later be sorted
private struct MeasuredSubview {
let view: LayoutSubview
let index: Int
let min: CGSize
let max: CGSize
let infiniteMainAxis: Bool
let spacing: CGFloat
}
/// The protocol all built-in stacks conform to.
/// Provides a shared implementation for stack layout logic.
@_spi(TokamakCore)
public protocol StackLayout: Layout where Cache == StackLayoutCache {
/// The direction of this stack. `vertical` for `VStack`, `horizontal` for `HStack`.
static var orientation: Axis { get }
/// The full `Alignment` with an ignored value for the main axis.
var _alignment: Alignment { get }
var spacing: CGFloat? { get }
}
@_spi(TokamakCore)
public extension StackLayout {
static var layoutProperties: LayoutProperties {
var properties = LayoutProperties()
properties.stackOrientation = Self.orientation
return properties
}
/// The `CGSize` component for the current `axis`.
///
/// A `vertical` axis will return `height`.
/// A `horizontal` axis will return `width`.
static var mainAxis: WritableKeyPath<CGSize, CGFloat> {
switch Self.orientation {
case .vertical: return \.height
case .horizontal: return \.width
}
}
/// The `CGSize` component for the axis opposite `axis`.
///
/// A `vertical` axis will return `width`.
/// A `horizontal` axis will return `height`.
static var crossAxis: WritableKeyPath<CGSize, CGFloat> {
switch Self.orientation {
case .vertical: return \.width
case .horizontal: return \.height
}
}
func makeCache(subviews: Subviews) -> Cache {
// Ensure we have enough space in `idealSizes` for each subview.
.init(maxSubview: nil, idealSizes: Array(repeating: .zero, count: subviews.count))
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
cache.maxSubview = nil
// Ensure we have enough space in `idealSizes` for each subview.
cache.idealSizes = Array(repeating: .zero, count: subviews.count)
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
let proposal = proposal.replacingUnspecifiedDimensions()
/// The minimum size of each `View` on the main axis.
var minSize = CGFloat.zero
/// The aggregate `ViewSpacing` distances.
var totalSpacing = CGFloat.zero
/// The number of `View`s with a given priority.
var priorityCount = [Double: Int]()
/// The aggregate minimum size of each `View` with a given priority.
var prioritySize = [Double: CGFloat]()
let measuredSubviews = subviews.enumerated().map { index, view -> MeasuredSubview in
priorityCount[view.priority, default: 0] += 1
var minProposal = CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
minProposal[keyPath: Self.crossAxis] = proposal[keyPath: Self.crossAxis]
minProposal[keyPath: Self.mainAxis] = 0
/// The minimum size for this subview along the `mainAxis`.
/// Uses `dimensions(in:)` to collect the alignment guides for use in `placeSubviews`.
let min = view.dimensions(in: .init(minProposal))
// Aggregate the minimum size of the stack for the combined subviews.
minSize += min.size[keyPath: Self.mainAxis]
// Aggregate the minimum size of this priority to divvy up space later.
prioritySize[view.priority, default: 0] += min.size[keyPath: Self.mainAxis]
var maxProposal = CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
maxProposal[keyPath: Self.crossAxis] = minProposal[keyPath: Self.crossAxis]
/// The maximum size for this subview along the `mainAxis`.
let max = view.sizeThatFits(.init(maxProposal))
/// The spacing around this `View` and its previous (if it is not first).
let spacing: CGFloat
if subviews.indices.contains(index - 1) {
if let overrideSpacing = self.spacing {
spacing = overrideSpacing
} else {
spacing = subviews[index - 1].spacing.distance(to: view.spacing, along: Self.orientation)
}
} else {
spacing = .zero
}
// Aggregate all spacing values.
totalSpacing += spacing
// If this `View` is the widest, save it to the cache for access in `placeSubviews`.
if min.size[keyPath: Self.crossAxis] > cache.maxSubview?.size[keyPath: Self.crossAxis]
?? .zero
{
cache.maxSubview = min
}
return MeasuredSubview(
view: view,
index: index,
min: min.size,
max: max,
infiniteMainAxis: max[keyPath: Self.mainAxis] == .infinity,
spacing: spacing
)
}
// Calculate ideal sizes for each View based on their min/max sizes and the space available.
var available = proposal[keyPath: Self.mainAxis] - minSize - totalSpacing
// The final resulting size.
var size = CGSize.zero
size[keyPath: Self.crossAxis] = cache.maxSubview?.size[keyPath: Self.crossAxis] ?? .zero
for subview in measuredSubviews.sorted(by: {
// Sort by priority descending.
if $0.view.priority == $1.view.priority {
// If the priorities match, allow non-flexible `View`s to size first.
return $1.infiniteMainAxis && !$0.infiniteMainAxis
} else {
return $0.view.priority > $1.view.priority
}
}) {
// The amount of space available to `View`s with this priority value.
let priorityAvailable = available + prioritySize[subview.view.priority, default: 0]
// The number of `View`s with this priority value remaining as a `CGFloat`.
let priorityRemaining = CGFloat(priorityCount[subview.view.priority, default: 1])
// Propose the full `crossAxis`, but only the remaining `mainAxis`.
// Divvy up the available space between each remaining `View` with this priority value.
var divviedSize = proposal
divviedSize[keyPath: Self.mainAxis] = priorityAvailable / priorityRemaining
let idealSize = subview.view.sizeThatFits(.init(divviedSize))
cache.idealSizes[subview.index] = idealSize
size[keyPath: Self.mainAxis] += idealSize[keyPath: Self.mainAxis] + subview.spacing
// Remove our `idealSize` from the `available` space.
available -= idealSize[keyPath: Self.mainAxis]
// Decrement the number of `View`s left with this priority so space can be evenly divided
// between the remaining `View`s.
priorityCount[subview.view.priority, default: 1] -= 1
}
return size
}
func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing {
subviews.reduce(into: .zero) { $0.formUnion($1.spacing) }
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
// The current progress along the `mainAxis`.
var position = CGFloat.zero
// The offset of the `_alignment` in the `maxSubview`,
// used as the reference point for alignments along this axis.
let alignmentOffset = cache.maxSubview?[alignment: _alignment, in: Self.orientation] ?? .zero
for (index, view) in subviews.enumerated() {
// Add a gap for the spacing distance from the previous subview to this one.
let spacing: CGFloat
if subviews.indices.contains(index - 1) {
if let overrideSpacing = self.spacing {
spacing = overrideSpacing
} else {
spacing = subviews[index - 1].spacing.distance(to: view.spacing, along: Self.orientation)
}
} else {
spacing = .zero
}
position += spacing
let proposal = ProposedViewSize(cache.idealSizes[index])
let size = view.dimensions(in: proposal)
// Offset the placement along the `crossAxis` to align with the
// `alignment` of the `maxSubview`.
var placement = CGSize(width: bounds.minX, height: bounds.minY)
placement[keyPath: Self.mainAxis] += position
placement[keyPath: Self.crossAxis] += alignmentOffset
- size[alignment: _alignment, in: Self.orientation]
view.place(
at: .init(
x: placement.width,
y: placement.height
),
proposal: proposal
)
// Move further along the stack's `mainAxis`.
position += size.size[keyPath: Self.mainAxis]
}
}
}
@_spi(TokamakCore)
extension VStack: StackLayout {
public static var orientation: Axis { .vertical }
public var _alignment: Alignment { .init(horizontal: alignment, vertical: .center) }
}
@_spi(TokamakCore)
extension HStack: StackLayout {
public static var orientation: Axis { .horizontal }
public var _alignment: Alignment { .init(horizontal: .center, vertical: alignment) }
}

View File

@ -1,105 +0,0 @@
// Copyright 2022 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 6/20/22.
//
import Foundation
/// The preferred spacing around a `View`.
///
/// When computing spacing in a custom `Layout`, use `distance(to:along:)`
/// to find the smallest spacing needed to accommodate the preferences
/// of the `View`s you are aligning.
public struct ViewSpacing {
/// The `View` type this `ViewSpacing` is for.
/// Some `View`s prefer different spacing based on the `View` they are adjacent to.
@_spi(TokamakCore)
public var viewType: Any.Type?
private var top: (ViewSpacing) -> CGFloat
private var leading: (ViewSpacing) -> CGFloat
private var bottom: (ViewSpacing) -> CGFloat
private var trailing: (ViewSpacing) -> CGFloat
public static let zero: ViewSpacing = .init(
viewType: nil,
top: { _ in 0 },
leading: { _ in 0 },
bottom: { _ in 0 },
trailing: { _ in 0 }
)
/// Create a `ViewSpacing` instance with default values.
public init() {
self.init(viewType: nil)
}
@_spi(TokamakCore)
public static let defaultValue: CGFloat = 8
@_spi(TokamakCore)
public init(
viewType: Any.Type?,
top: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
leading: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
bottom: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
trailing: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue }
) {
self.viewType = viewType
self.top = top
self.leading = leading
self.bottom = bottom
self.trailing = trailing
}
public mutating func formUnion(_ other: ViewSpacing, edges: Edge.Set = .all) {
if viewType != other.viewType {
viewType = nil
}
if edges.contains(.top) {
let current = top
top = { max(current($0), other.top($0)) }
}
if edges.contains(.leading) {
let current = leading
leading = { max(current($0), other.leading($0)) }
}
if edges.contains(.bottom) {
let current = bottom
bottom = { max(current($0), other.bottom($0)) }
}
if edges.contains(.trailing) {
let current = trailing
trailing = { max(current($0), other.trailing($0)) }
}
}
public func union(_ other: ViewSpacing, edges: Edge.Set = .all) -> ViewSpacing {
var spacing = self
spacing.formUnion(other, edges: edges)
return spacing
}
/// The smallest spacing that accommodates the preferences of `self` and `next`.
public func distance(to next: ViewSpacing, along axis: Axis) -> CGFloat {
// Assume `next` comes after `self` either horizontally or vertically.
switch axis {
case .horizontal:
return max(trailing(next), next.leading(self))
case .vertical:
return max(bottom(next), next.top(self))
}
}
}

View File

@ -1,106 +0,0 @@
// Copyright 2022 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 6/27/22.
//
import Foundation
private struct AspectRatioLayout: Layout {
let aspectRatio: CGFloat?
let contentMode: ContentMode
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let proposal = proposal.replacingUnspecifiedDimensions()
let aspectRatio: CGFloat
if let ratio = self.aspectRatio {
aspectRatio = ratio
} else {
let idealSubviewSize = subviews.first?.sizeThatFits(.unspecified) ?? .zero
if idealSubviewSize.height == 0 {
aspectRatio = 0
} else {
aspectRatio = idealSubviewSize.width / idealSubviewSize.height
}
}
let maxAxis: Axis
switch contentMode {
case .fit:
if proposal.width == proposal.height {
if aspectRatio >= 1 {
maxAxis = .vertical
} else {
maxAxis = .horizontal
}
} else if proposal.width > proposal.height {
maxAxis = .horizontal
} else {
maxAxis = .vertical
}
case .fill:
if proposal.width == proposal.height {
if aspectRatio >= 1 {
maxAxis = .horizontal
} else {
maxAxis = .vertical
}
} else if proposal.width > proposal.height {
maxAxis = .vertical
} else {
maxAxis = .horizontal
}
}
switch maxAxis {
case .horizontal:
return .init(
width: aspectRatio * proposal.height,
height: proposal.height
)
case .vertical:
return .init(
width: proposal.width,
height: (1 / aspectRatio) * proposal.width
)
}
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
for subview in subviews {
subview.place(
at: .init(x: bounds.midX, y: bounds.midY),
anchor: .center,
proposal: .init(bounds.size)
)
}
}
}
public extension _AspectRatioLayout {
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
visitor.visit(
AspectRatioLayout(
aspectRatio: aspectRatio,
contentMode: contentMode
)
.callAsFunction {
content
}
)
}
}

View File

@ -1,139 +0,0 @@
// Copyright 2022 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 6/20/22.
//
import Foundation
/// A `Layout` container that creates a frame with constraints.
///
/// The children are proposed the full proposal given to this container
/// clamped to the specified minimum and maximum values.
///
/// Then the children are placed with `alignment` in the container.
private struct FlexFrameLayout: Layout {
let minWidth: CGFloat?
let idealWidth: CGFloat?
let maxWidth: CGFloat?
let minHeight: CGFloat?
let idealHeight: CGFloat?
let maxHeight: CGFloat?
let alignment: Alignment
struct Cache {
var dimensions = [ViewDimensions]()
}
func makeCache(subviews: Subviews) -> Cache {
.init()
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
cache.dimensions.removeAll()
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
let bounds = CGSize(
width: min(
max(minWidth ?? .zero, proposal.width ?? idealWidth ?? .zero),
maxWidth ?? CGFloat.infinity
),
height: min(
max(minHeight ?? .zero, proposal.height ?? idealHeight ?? .zero),
maxHeight ?? CGFloat.infinity
)
)
let proposal = ProposedViewSize(bounds)
var subviewSizes = CGSize.zero
cache.dimensions = subviews.map { subview -> ViewDimensions in
let dimensions = subview.dimensions(in: proposal)
if dimensions.width > subviewSizes.width {
subviewSizes.width = dimensions.width
}
if dimensions.height > subviewSizes.height {
subviewSizes.height = dimensions.height
}
return dimensions
}
var size = CGSize.zero
if let minWidth = minWidth,
bounds.width < subviewSizes.width
{
size.width = max(bounds.width, minWidth)
} else if let maxWidth = maxWidth,
bounds.width > subviewSizes.width
{
size.width = min(bounds.width, maxWidth)
} else {
size.width = subviewSizes.width
}
if let minHeight = minHeight,
bounds.height < subviewSizes.height
{
size.height = max(bounds.height, minHeight)
} else if let maxHeight = maxHeight,
bounds.height > subviewSizes.height
{
size.height = min(bounds.height, maxHeight)
} else {
size.height = subviewSizes.height
}
return size
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
let proposal = ProposedViewSize(bounds.size)
let frameDimensions = ViewDimensions(
size: .init(width: bounds.width, height: bounds.height),
alignmentGuides: [:]
)
for (index, subview) in subviews.enumerated() {
subview.place(
at: .init(
x: bounds.minX + frameDimensions[alignment.horizontal]
- cache.dimensions[index][alignment.horizontal],
y: bounds.minY + frameDimensions[alignment.vertical]
- cache.dimensions[index][alignment.vertical]
),
proposal: proposal
)
}
}
}
public extension _FlexFrameLayout {
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
visitor.visit(FlexFrameLayout(
minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth,
minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight,
alignment: alignment
).callAsFunction {
content
})
}
}

View File

@ -1,103 +0,0 @@
// Copyright 2022 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 5/28/22.
//
import Foundation
/// A `Layout` container that requests a specific size on one or more axes.
///
/// The container proposes the constrained size to its children,
/// then places them with `alignment` in the constrained bounds.
///
/// Children request their own size, so they may overflow this container.
///
/// If no fixed size is specified for a an axis, the container will use the size of its children.
private struct FrameLayout: Layout {
let width: CGFloat?
let height: CGFloat?
let alignment: Alignment
struct Cache {
var dimensions = [ViewDimensions]()
}
func makeCache(subviews: Subviews) -> Cache {
.init()
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
cache.dimensions.removeAll()
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
var size = CGSize.zero
let proposal = ProposedViewSize(
width: width ?? proposal.width,
height: height ?? proposal.height
)
cache.dimensions = subviews.map { subview -> ViewDimensions in
let dimensions = subview.dimensions(in: proposal)
if dimensions.width > size.width {
size.width = dimensions.width
}
if dimensions.height > size.height {
size.height = dimensions.height
}
return dimensions
}
return .init(
width: width ?? size.width,
height: height ?? size.height
)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
let proposal = ProposedViewSize(bounds.size)
let frameDimensions = ViewDimensions(
size: .init(width: bounds.width, height: bounds.height),
alignmentGuides: [:]
)
for (index, subview) in subviews.enumerated() {
subview.place(
at: .init(
x: bounds.minX + frameDimensions[alignment.horizontal]
- cache.dimensions[index][alignment.horizontal],
y: bounds.minY + frameDimensions[alignment.vertical]
- cache.dimensions[index][alignment.vertical]
),
proposal: proposal
)
}
}
}
public extension _FrameLayout {
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
visitor.visit(FrameLayout(width: width, height: height, alignment: alignment).callAsFunction {
content
})
}
}

View File

@ -1,38 +0,0 @@
// Copyright 2022 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 2/15/22.
//
import Foundation
public enum Mutation<Renderer: FiberRenderer> {
case insert(
element: Renderer.ElementType,
parent: Renderer.ElementType,
index: Int
)
case remove(element: Renderer.ElementType, parent: Renderer.ElementType?)
case replace(
parent: Renderer.ElementType,
previous: Renderer.ElementType,
replacement: Renderer.ElementType
)
case update(
previous: Renderer.ElementType,
newContent: Renderer.ElementType.Content,
geometry: ViewGeometry
)
case layout(element: Renderer.ElementType, geometry: ViewGeometry)
}

View File

@ -1,136 +0,0 @@
// Copyright 2022 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 6/16/22.
//
import Foundation
extension FiberReconciler {
final class Caches {
var elementIndices = [ObjectIdentifier: Int]()
var layoutCaches = [ObjectIdentifier: LayoutCache]()
var layoutSubviews = [ObjectIdentifier: LayoutSubviews]()
var mutations = [Mutation<Renderer>]()
struct LayoutCache {
/// The erased `Layout.Cache` value.
var cache: AnyLayout.Cache
/// Cached values for `sizeThatFits` calls.
var sizeThatFits: [SizeThatFitsRequest: CGSize]
/// Cached values for `dimensions(in:)` calls.
var dimensions: [SizeThatFitsRequest: ViewDimensions]
/// Does this cache need to be updated before using?
/// Set to `true` whenever the subviews or the container changes.
var isDirty: Bool
/// Empty the cached values and flag the cache as dirty.
mutating func markDirty() {
isDirty = true
sizeThatFits.removeAll()
dimensions.removeAll()
}
struct SizeThatFitsRequest: Hashable {
let proposal: ProposedViewSize
@inlinable
init(_ proposal: ProposedViewSize) {
self.proposal = proposal
}
func hash(into hasher: inout Hasher) {
hasher.combine(proposal.width)
hasher.combine(proposal.height)
}
}
}
func clear() {
elementIndices.removeAll()
layoutSubviews.removeAll()
mutations.removeAll()
}
func layoutCache(for fiber: Fiber) -> LayoutCache? {
guard let layout = fiber.layout else { return nil }
return layoutCaches[
ObjectIdentifier(fiber),
default: .init(
cache: layout.makeCache(subviews: layoutSubviews(for: fiber)),
sizeThatFits: [:],
dimensions: [:],
isDirty: false
)
]
}
func updateLayoutCache<R>(for fiber: Fiber, _ action: (inout LayoutCache) -> R) -> R? {
guard let layout = fiber.layout else { return nil }
let subviews = layoutSubviews(for: fiber)
let key = ObjectIdentifier(fiber)
var cache = layoutCaches[
key,
default: .init(
cache: layout.makeCache(subviews: subviews),
sizeThatFits: [:],
dimensions: [:],
isDirty: false
)
]
// If the cache is dirty, update it before calling `action`.
if cache.isDirty {
layout.updateCache(&cache.cache, subviews: subviews)
cache.isDirty = false
}
defer { layoutCaches[key] = cache }
return action(&cache)
}
func layoutSubviews(for fiber: Fiber) -> LayoutSubviews {
layoutSubviews[ObjectIdentifier(fiber), default: .init(fiber)]
}
func elementIndex(for fiber: Fiber, increment: Bool = false) -> Int {
let key = ObjectIdentifier(fiber)
let result = elementIndices[key, default: 0]
if increment {
elementIndices[key] = result + 1
}
return result
}
}
}
protocol FiberReconcilerPass {
/// Run this pass with the given inputs.
///
/// - Parameter reconciler: The `FiberReconciler` running this pass.
/// - Parameter root: The node to start the pass from.
/// The top of the `View` hierarchy when `useDynamicLayout` is enabled.
/// Otherwise, the same as `reconcileRoot`.
/// - Parameter reconcileRoot: A list of topmost nodes that need reconciliation.
/// When `useDynamicLayout` is enabled, this can be used to limit
/// the number of operations performed during reconciliation.
/// - Parameter caches: The shared cache data for this and other passes.
func run<R: FiberRenderer>(
in reconciler: FiberReconciler<R>,
root: FiberReconciler<R>.TreeReducer.Result,
changedFibers: Set<ObjectIdentifier>,
caches: FiberReconciler<R>.Caches
)
}

View File

@ -1,69 +0,0 @@
// Copyright 2022 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 6/16/22.
//
import Foundation
// Layout from the top down.
struct LayoutPass: FiberReconcilerPass {
func run<R>(
in reconciler: FiberReconciler<R>,
root: FiberReconciler<R>.TreeReducer.Result,
changedFibers: Set<ObjectIdentifier>,
caches: FiberReconciler<R>.Caches
) where R: FiberRenderer {
guard let root = root.fiber else { return }
var fiber = root
while true {
// Place subviews for each element fiber as we walk the tree.
if fiber.element != nil {
caches.updateLayoutCache(for: fiber) { cache in
fiber.layout?.placeSubviews(
in: .init(
origin: .zero,
size: fiber.geometry?.dimensions.size ?? reconciler.renderer.sceneSize.value
),
proposal: fiber.geometry?.proposal ?? .unspecified,
subviews: caches.layoutSubviews(for: fiber),
cache: &cache.cache
)
}
}
if let child = fiber.child {
// Continue down the tree.
fiber = child
continue
}
while fiber.sibling == nil {
// Exit at the top of the `View` tree
guard let parent = fiber.parent else { return }
guard parent !== root else { return }
// Walk up to the next parent.
fiber = parent
}
// Walk across to the next sibling.
fiber = fiber.sibling!
}
}
}
extension FiberReconcilerPass where Self == LayoutPass {
static var layout: LayoutPass { .init() }
}

View File

@ -1,296 +0,0 @@
// Copyright 2022 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 6/16/22.
//
import Foundation
/// Walk the current tree, recomputing at each step to check for discrepancies.
///
/// Parent-first depth-first traversal.
/// Take this `View` tree for example.
/// ```swift
/// VStack {
/// HStack {
/// Text("A")
/// Text("B")
/// }
/// Text("C")
/// }
/// ```
/// Basically, we read it like this:
/// 1. `VStack` has children, so we go to it's first child, `HStack`.
/// 2. `HStack` has children, so we go further to it's first child, `Text`.
/// 3. `Text` has no child, but has a sibling, so we go to that.
/// 4. `Text` has no child and no sibling, so we return to the `HStack`.
/// 5. We've already read the children, so we look for a sibling, `Text`.
/// 6. `Text` has no children and no sibling, so we return to the `VStack.`
/// We finish once we've returned to the root element.
/// ```
///
/// VStack
///
/// 1
///
/// HStack
///
/// 2
///
/// Text
/// 6 4
/// 3
/// 5
/// Text
///
///
///
/// Text
///
/// ```
struct ReconcilePass: FiberReconcilerPass {
func run<R>(
in reconciler: FiberReconciler<R>,
root: FiberReconciler<R>.TreeReducer.Result,
changedFibers: Set<ObjectIdentifier>,
caches: FiberReconciler<R>.Caches
) where R: FiberRenderer {
var node = root
// Enabled when we reach the `reconcileRoot`.
var shouldReconcile = false
while true {
if !shouldReconcile {
if let fiber = node.fiber,
changedFibers.contains(ObjectIdentifier(fiber))
{
shouldReconcile = true
} else if let alternate = node.fiber?.alternate,
changedFibers.contains(ObjectIdentifier(alternate))
{
shouldReconcile = true
}
}
// If this fiber has an element, set its `elementIndex`
// and increment the `elementIndices` value for its `elementParent`.
if node.fiber?.element != nil,
let elementParent = node.fiber?.elementParent
{
node.fiber?.elementIndex = caches.elementIndex(for: elementParent, increment: true)
}
// Perform work on the node.
if shouldReconcile,
let mutation = reconcile(node, in: reconciler, caches: caches)
{
caches.mutations.append(mutation)
}
// Ensure the `TreeReducer` can access any necessary state.
node.elementIndices = caches.elementIndices
// Pass view traits down to the nearest element fiber.
if let traits = node.fiber?.outputs.traits,
!traits.values.isEmpty
{
node.nextTraits.values.merge(traits.values, uniquingKeysWith: { $1 })
}
// Update `DynamicProperty`s before accessing the `View`'s body.
node.fiber?.updateDynamicProperties()
// Compute the children of the node.
let reducer = FiberReconciler<R>.TreeReducer.SceneVisitor(initialResult: node)
node.visitChildren(reducer)
node.fiber?.preferences?.reset()
if reconciler.renderer.useDynamicLayout,
let fiber = node.fiber
{
if let element = fiber.element,
let elementParent = fiber.elementParent
{
let parentKey = ObjectIdentifier(elementParent)
let subview = LayoutSubview(
id: ObjectIdentifier(fiber),
traits: fiber.outputs.traits,
fiber: fiber,
element: element,
caches: caches
)
caches.layoutSubviews[parentKey, default: .init(elementParent)].storage.append(subview)
}
}
// Setup the alternate if it doesn't exist yet.
if node.fiber?.alternate == nil {
_ = node.fiber?.createAndBindAlternate?()
}
// Walk down all the way into the deepest child.
if let child = reducer.result.child {
node = child
continue
} else if let alternateChild = node.fiber?.alternate?.child {
// The alternate has a child that no longer exists.
if let parent = node.fiber?.element != nil ? node.fiber : node.fiber?.elementParent {
invalidateCache(for: parent, in: reconciler, caches: caches)
}
walk(alternateChild) { node in
if let element = node.element,
let parent = node.elementParent?.element
{
// Removals must happen in reverse order, so a child element
// is removed before its parent.
caches.mutations.insert(.remove(element: element, parent: parent), at: 0)
}
return true
}
}
if reducer.result.child == nil {
// Make sure we clear the child if there was none
node.fiber?.child = nil
node.fiber?.alternate?.child = nil
}
// If we've made it back to the root, then exit.
if node === root {
return
}
// Now walk back up the tree until we find a sibling.
while node.sibling == nil {
if let fiber = node.fiber,
fiber.element != nil
{
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
}
if let preferences = node.fiber?.preferences {
if let action = node.fiber?.outputs.preferenceAction {
action(preferences)
}
if let parentPreferences = node.fiber?.preferenceParent?.preferences {
parentPreferences.merge(preferences)
}
}
var alternateSibling = node.fiber?.alternate?.sibling
// The alternate had siblings that no longer exist.
while alternateSibling != nil {
if let fiber = alternateSibling?.elementParent {
invalidateCache(for: fiber, in: reconciler, caches: caches)
}
if let element = alternateSibling?.element,
let parent = alternateSibling?.elementParent?.element
{
// Removals happen in reverse order, so a child element is removed before
// its parent.
caches.mutations.insert(.remove(element: element, parent: parent), at: 0)
}
alternateSibling = alternateSibling?.sibling
}
guard let parent = node.parent else { return }
// When we walk back to the root, exit
guard parent !== root.fiber?.alternate else { return }
node = parent
}
if let fiber = node.fiber {
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
}
// Walk across to the sibling, and repeat.
node = node.sibling!
}
}
/// Compare `node` with its alternate, and add any mutations to the list.
func reconcile<R: FiberRenderer>(
_ node: FiberReconciler<R>.TreeReducer.Result,
in reconciler: FiberReconciler<R>,
caches: FiberReconciler<R>.Caches
) -> Mutation<R>? {
if let element = node.fiber?.element,
let index = node.fiber?.elementIndex,
let parent = node.fiber?.elementParent?.element
{
if node.fiber?.alternate == nil { // This didn't exist before (no alternate)
if let fiber = node.fiber {
invalidateCache(for: fiber, in: reconciler, caches: caches)
}
return .insert(element: element, parent: parent, index: index)
} else if node.fiber?.typeInfo?.type != node.fiber?.alternate?.typeInfo?.type,
let previous = node.fiber?.alternate?.element
{
if let fiber = node.fiber {
invalidateCache(for: fiber, in: reconciler, caches: caches)
}
// This is a completely different type of view.
return .replace(parent: parent, previous: previous, replacement: element)
} else if let newContent = node.newContent,
newContent != element.content
{
if let fiber = node.fiber {
invalidateCache(for: fiber, in: reconciler, caches: caches)
}
// This is the same type of view, but its backing data has changed.
return .update(
previous: element,
newContent: newContent,
geometry: node.fiber?.geometry ?? .init(
origin: .init(origin: .zero),
dimensions: .init(size: .zero, alignmentGuides: [:]),
proposal: .unspecified
)
)
}
}
return nil
}
/// Remove cached size values if something changed.
func invalidateCache<R: FiberRenderer>(
for fiber: FiberReconciler<R>.Fiber,
in reconciler: FiberReconciler<R>,
caches: FiberReconciler<R>.Caches
) {
guard reconciler.renderer.useDynamicLayout else { return }
caches.updateLayoutCache(for: fiber) { cache in
cache.markDirty()
}
if let alternate = fiber.alternate {
caches.updateLayoutCache(for: alternate) { cache in
cache.markDirty()
}
}
}
@inlinable
func propagateCacheInvalidation<R: FiberRenderer>(
for fiber: FiberReconciler<R>.Fiber,
in reconciler: FiberReconciler<R>,
caches: FiberReconciler<R>.Caches
) {
guard caches.layoutCache(for: fiber)?.isDirty ?? false,
let elementParent = fiber.elementParent
else { return }
invalidateCache(for: elementParent, in: reconciler, caches: caches)
}
}
extension FiberReconcilerPass where Self == ReconcilePass {
static var reconcile: ReconcilePass { ReconcilePass() }
}

View File

@ -1,25 +0,0 @@
// Copyright 2022 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 5/30/22.
//
import Foundation
public extension Scene {
// By default, we simply pass the inputs through without modifications.
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs {
.init(inputs: inputs)
}
}

View File

@ -1,68 +0,0 @@
// Copyright 2022 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 5/30/22.
//
/// A type that can visit a `Scene`.
public protocol SceneVisitor: ViewVisitor {
func visit<S: Scene>(_ scene: S)
}
public extension Scene {
func _visitChildren<V: SceneVisitor>(_ visitor: V) {
visitor.visit(body)
}
}
/// A type that creates a `Result` by visiting multiple `Scene`s.
protocol SceneReducer: ViewReducer {
associatedtype Result
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S)
static func reduce<S: Scene>(partialResult: Result, nextScene: S) -> Result
}
extension SceneReducer {
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S) {
partialResult = Self.reduce(partialResult: partialResult, nextScene: nextScene)
}
static func reduce<S: Scene>(partialResult: Result, nextScene: S) -> Result {
var result = partialResult
Self.reduce(into: &result, nextScene: nextScene)
return result
}
}
/// A `SceneVisitor` that uses a `SceneReducer`
/// to collapse the `Scene` values into a single `Result`.
final class SceneReducerVisitor<R: SceneReducer>: SceneVisitor {
var result: R.Result
init(initialResult: R.Result) {
result = initialResult
}
func visit<S>(_ scene: S) where S: Scene {
R.reduce(into: &result, nextScene: scene)
}
func visit<V>(_ view: V) where V: View {
R.reduce(into: &result, nextView: view)
}
}
extension SceneReducer {
typealias SceneVisitor = SceneReducerVisitor<Self>
}

View File

@ -1,101 +0,0 @@
// Copyright 2022 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 2/7/22.
//
import Foundation
/// Data passed to `_makeView` to create the `ViewOutputs` used in reconciling/rendering.
public struct ViewInputs<V> {
public let content: V
/// Mutate the underlying content with the given inputs.
///
/// Used to inject values such as environment values, traits, and preferences into the `View` type.
public let updateContent: ((inout V) -> ()) -> ()
@_spi(TokamakCore)
public let environment: EnvironmentBox
public let traits: _ViewTraitStore?
public let preferenceStore: _PreferenceStore?
}
/// Data used to reconcile and render a `View` and its children.
public struct ViewOutputs {
/// A container for the current `EnvironmentValues`.
/// This is stored as a reference to avoid copying the environment when unnecessary.
let environment: EnvironmentBox
let preferenceStore: _PreferenceStore?
/// An action to perform after all preferences values have been reduced.
///
/// Called when walking back up the tree in the `ReconcilePass`.
let preferenceAction: ((_PreferenceStore) -> ())?
let traits: _ViewTraitStore?
}
@_spi(TokamakCore)
public final class EnvironmentBox {
public let environment: EnvironmentValues
public init(_ environment: EnvironmentValues) {
self.environment = environment
}
}
public extension ViewOutputs {
init<V>(
inputs: ViewInputs<V>,
environment: EnvironmentValues? = nil,
preferenceStore: _PreferenceStore? = nil,
preferenceAction: ((_PreferenceStore) -> ())? = nil,
traits: _ViewTraitStore? = nil
) {
// Only replace the `EnvironmentBox` when we change the environment.
// Otherwise the same box can be reused.
self.environment = environment.map(EnvironmentBox.init) ?? inputs.environment
self.preferenceStore = preferenceStore
self.preferenceAction = preferenceAction
self.traits = traits ?? inputs.traits
}
}
public extension View {
// By default, we simply pass the inputs through without modifications
// or layout considerations.
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(inputs: inputs)
}
}
public extension ModifiedContent where Content: View, Modifier: ViewModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
Modifier._makeView(.init(
content: inputs.content.modifier,
updateContent: { _ in },
environment: inputs.environment,
traits: inputs.traits,
preferenceStore: inputs.preferenceStore
))
}
func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
modifier._visitChildren(visitor, content: .init(modifier: modifier, view: content))
}
}

View File

@ -1,66 +0,0 @@
// Copyright 2022 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 2/17/22.
//
import Foundation
public struct ViewGeometry: Equatable {
@_spi(TokamakCore)
public let origin: ViewOrigin
@_spi(TokamakCore)
public let dimensions: ViewDimensions
let proposal: ProposedViewSize
}
/// The position of the `View` relative to its parent.
public struct ViewOrigin: Equatable {
@_spi(TokamakCore)
public let origin: CGPoint
@_spi(TokamakCore)
public var x: CGFloat { origin.x }
@_spi(TokamakCore)
public var y: CGFloat { origin.y }
}
public struct ViewDimensions: Equatable {
@_spi(TokamakCore)
public let size: CGSize
@_spi(TokamakCore)
public let alignmentGuides: [ObjectIdentifier: CGFloat]
public var width: CGFloat { size.width }
public var height: CGFloat { size.height }
public subscript(guide: HorizontalAlignment) -> CGFloat {
self[explicit: guide] ?? guide.id.defaultValue(in: self)
}
public subscript(guide: VerticalAlignment) -> CGFloat {
self[explicit: guide] ?? guide.id.defaultValue(in: self)
}
public subscript(explicit guide: HorizontalAlignment) -> CGFloat? {
alignmentGuides[.init(guide.id)]
}
public subscript(explicit guide: VerticalAlignment) -> CGFloat? {
alignmentGuides[.init(guide.id)]
}
}

View File

@ -1,66 +0,0 @@
// Copyright 2022 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 2/3/22.
//
/// A type that can visit a `View`.
public protocol ViewVisitor {
func visit<V: View>(_ view: V)
}
public extension View {
func _visitChildren<V: ViewVisitor>(_ visitor: V) {
visitor.visit(body)
}
}
public typealias ViewVisitorF<V: ViewVisitor> = (V) -> ()
/// A type that creates a `Result` by visiting multiple `View`s.
protocol ViewReducer {
associatedtype Result
static func reduce<V: View>(into partialResult: inout Result, nextView: V)
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result
}
extension ViewReducer {
static func reduce<V: View>(into partialResult: inout Result, nextView: V) {
partialResult = Self.reduce(partialResult: partialResult, nextView: nextView)
}
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result {
var result = partialResult
Self.reduce(into: &result, nextView: nextView)
return result
}
}
/// A `ViewVisitor` that uses a `ViewReducer`
/// to collapse the `View` values into a single `Result`.
final class ReducerVisitor<R: ViewReducer>: ViewVisitor {
var result: R.Result
init(initialResult: R.Result) {
result = initialResult
}
func visit<V>(_ view: V) where V: View {
R.reduce(into: &result, nextView: view)
}
}
extension ViewReducer {
typealias Visitor = ReducerVisitor<Self>
}

View File

@ -1,86 +0,0 @@
// Copyright 2022 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 2/11/22.
//
@_spi(TokamakCore)
public enum WalkWorkResult<Success> {
case `continue`
case `break`(with: Success)
case pause
}
@_spi(TokamakCore)
public enum WalkResult<Renderer: FiberRenderer, Success> {
case success(Success)
case finished
case paused(at: FiberReconciler<Renderer>.Fiber)
}
/// Walk a fiber tree from `root` until the `work` predicate returns `false`.
@_spi(TokamakCore)
@discardableResult
public func walk<Renderer: FiberRenderer>(
_ root: FiberReconciler<Renderer>.Fiber,
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> Bool
) rethrows -> WalkResult<Renderer, ()> {
try walk(root) {
try work($0) ? .continue : .pause
}
}
/// Parent-first depth-first traversal of a `Fiber` tree.
/// `work` is called with each `Fiber` in the tree as they are entered.
///
/// Traversal uses the following process:
/// 1. Perform work on the current `Fiber`.
/// 2. If the `Fiber` has a child, repeat from (1) with the child.
/// 3. If the `Fiber` does not have a sibling, walk up until we find a `Fiber` that does have one.
/// 4. Walk across to the sibling.
///
/// When the `root` is reached, the loop exits.
@_spi(TokamakCore)
public func walk<Renderer: FiberRenderer, Success>(
_ root: FiberReconciler<Renderer>.Fiber,
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> WalkWorkResult<Success>
) rethrows -> WalkResult<Renderer, Success> {
var current = root
while true {
// Perform work on the node
switch try work(current) {
case .continue: break
case let .break(success): return .success(success)
case .pause: return .paused(at: current)
}
// Walk into the child
if let child = current.child {
current = child
continue
}
// When we walk back to the root, exit
if current === root {
return .finished
}
// Walk back up until we find a sibling
while current.sibling == nil {
// When we walk back to the root, exit
guard let parent = current.parent,
parent !== root else { return .finished }
current = parent
}
// Walk the sibling
current = current.sibling!
}
}

View File

@ -14,8 +14,7 @@
import Foundation import Foundation
@frozen @frozen public enum ContentMode: Hashable, CaseIterable {
public enum ContentMode: Hashable, CaseIterable {
case fit case fit
case fill case fill
} }

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct _OffsetEffect: GeometryEffect, Equatable {
public struct _OffsetEffect: GeometryEffect, Equatable {
public var offset: CGSize public var offset: CGSize
@inlinable @inlinable

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct _ScaleEffect: GeometryEffect, Equatable {
public struct _ScaleEffect: GeometryEffect, Equatable {
public var scale: CGSize public var scale: CGSize
public var anchor: UnitPoint public var anchor: UnitPoint

View File

@ -13,16 +13,14 @@
// limitations under the License. // limitations under the License.
protocol ModifierContainer { protocol ModifierContainer {
var environmentModifier: _EnvironmentModifier? { get } var environmentModifier: EnvironmentModifier? { get }
} }
protocol ModifiedContentProtocol {} protocol ModifiedContentProtocol {}
/// A value with a modifier applied to it. /// A value with a modifier applied to it.
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol { public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
@Environment(\.self) @Environment(\.self) public var environment
public var environment
public typealias Body = Never public typealias Body = Never
public private(set) var content: Content public private(set) var content: Content
public private(set) var modifier: Modifier public private(set) var modifier: Modifier
@ -34,7 +32,7 @@ public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
} }
extension ModifiedContent: ModifierContainer { extension ModifiedContent: ModifierContainer {
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier } var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
} }
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader { extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {

View File

@ -24,18 +24,6 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
public let content: Content public let content: Content
public let background: Background public let background: Background
public let alignment: Alignment public let alignment: Alignment
@_spi(TokamakCore)
public init(content: Content, background: Background, alignment: Alignment) {
self.content = content
self.background = background
self.alignment = alignment
}
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(background)
visitor.visit(content)
}
} }
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
@ -89,8 +77,7 @@ public extension View {
} }
} }
@frozen @frozen public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
where Style: ShapeStyle, Bounds: Shape where Style: ShapeStyle, Bounds: Shape
{ {
public var environment: EnvironmentValues! public var environment: EnvironmentValues!
@ -142,11 +129,6 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
public let content: Content public let content: Content
public let overlay: Overlay public let overlay: Overlay
public let alignment: Alignment public let alignment: Alignment
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(content)
visitor.visit(overlay)
}
} }
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader

View File

@ -16,23 +16,6 @@ public protocol ViewModifier {
typealias Content = _ViewModifier_Content<Self> typealias Content = _ViewModifier_Content<Self>
associatedtype Body: View associatedtype Body: View
func body(content: Content) -> Self.Body func body(content: Content) -> Self.Body
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor
}
public extension ViewModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(inputs: inputs)
}
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
if Body.self == Never.self {
content.visitChildren(visitor)
} else {
visitor.visit(body(content: content))
}
}
} }
public struct _ViewModifier_Content<Modifier>: View public struct _ViewModifier_Content<Modifier>: View
@ -40,27 +23,15 @@ public struct _ViewModifier_Content<Modifier>: View
{ {
public let modifier: Modifier public let modifier: Modifier
public let view: AnyView public let view: AnyView
let visitChildren: (ViewVisitor) -> ()
public init(modifier: Modifier, view: AnyView) { public init(modifier: Modifier, view: AnyView) {
self.modifier = modifier self.modifier = modifier
self.view = view self.view = view
visitChildren = { $0.visit(view) }
}
public init<V: View>(modifier: Modifier, view: V) {
self.modifier = modifier
self.view = AnyView(view)
visitChildren = { $0.visit(view) }
} }
public var body: some View { public var body: some View {
view view
} }
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitChildren(visitor)
}
} }
public extension View { public extension View {

View File

@ -78,6 +78,9 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol { if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore) self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
if let parent = parent {
parent.preferenceStore.merge(with: self.preferenceStore)
}
} }
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol { if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {

View File

@ -94,9 +94,13 @@ public class MountedElement<R: Renderer> {
public internal(set) var environmentValues: EnvironmentValues public internal(set) var environmentValues: EnvironmentValues
private(set) weak var parent: MountedElement<R>? weak var parent: MountedElement<R>?
/// `didSet` on this field propagates the preference changes up the view tree.
var preferenceStore: _PreferenceStore = .init() var preferenceStore: _PreferenceStore = .init() {
didSet {
parent?.preferenceStore.merge(with: preferenceStore)
}
}
public internal(set) var viewTraits: _ViewTraitStore public internal(set) var viewTraits: _ViewTraitStore
@ -106,7 +110,6 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues self.environmentValues = environmentValues
viewTraits = .init() viewTraits = .init()
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) { init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
@ -115,7 +118,6 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues self.environmentValues = environmentValues
viewTraits = .init() viewTraits = .init()
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
init( init(
@ -129,7 +131,6 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues self.environmentValues = environmentValues
self.viewTraits = viewTraits self.viewTraits = viewTraits
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
func updateEnvironment() { func updateEnvironment() {
@ -144,10 +145,6 @@ public class MountedElement<R: Renderer> {
} }
} }
func connectParentPreferenceStore() {
preferenceStore.parent = parent?.preferenceStore
}
/// You must call `super.prepareForMount` before all other mounting work. /// You must call `super.prepareForMount` before all other mounting work.
func prepareForMount(with transaction: Transaction) { func prepareForMount(with transaction: Transaction) {
// `GroupView`'s don't really mount, so let their children transition if the group can. // `GroupView`'s don't really mount, so let their children transition if the group can.

View File

@ -25,41 +25,12 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
static var defaultValue: Value { nil } static var defaultValue: Value { nil }
} }
final class _PreferenceValueStorage: CustomDebugStringConvertible {
/// Every value the `Key` has had.
var valueList: [Any]
var debugDescription: String {
valueList.debugDescription
}
init<Key: PreferenceKey>(_ key: Key.Type = Key.self) {
valueList = []
}
init(valueList: [Any]) {
self.valueList = valueList
}
func merge(_ other: _PreferenceValueStorage) {
valueList.append(contentsOf: other.valueList)
}
func reset() {
valueList = []
}
}
public struct _PreferenceValue<Key> where Key: PreferenceKey { public struct _PreferenceValue<Key> where Key: PreferenceKey {
var storage: _PreferenceValueStorage /// Every value the `Key` has had.
var valueList: [Key.Value]
init(storage: _PreferenceValueStorage) {
self.storage = storage
}
/// The latest value. /// The latest value.
public var value: Key.Value { public var value: Key.Value {
reduce(storage.valueList.compactMap { $0 as? Key.Value }) reduce(valueList)
} }
func reduce(_ values: [Key.Value]) -> Key.Value { func reduce(_ values: [Key.Value]) -> Key.Value {
@ -77,79 +48,38 @@ public extension _PreferenceValue {
} }
} }
public final class _PreferenceStore: CustomDebugStringConvertible { public struct _PreferenceStore {
/// The values of the `_PreferenceStore` on the last update.
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
/// The backing values of the `_PreferenceStore`. /// The backing values of the `_PreferenceStore`.
private var values: [ObjectIdentifier: _PreferenceValueStorage] private var values: [String: Any]
weak var parent: _PreferenceStore? public init(values: [String: Any] = [:]) {
public var debugDescription: String {
"Preferences (\(ObjectIdentifier(self))): \(values)"
}
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
previousValues = [:]
self.values = values self.values = values
} }
/// Retrieve a late-binding token for `key`, or save the default value if it does not yet exist.
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key> public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
where Key: PreferenceKey where Key: PreferenceKey
{ {
let keyID = ObjectIdentifier(key) values[String(reflecting: key)] as? _PreferenceValue<Key>
let storage: _PreferenceValueStorage ?? _PreferenceValue(valueList: [Key.defaultValue])
if let existing = values[keyID] {
storage = existing
} else {
storage = .init(key)
values[keyID] = storage
}
return _PreferenceValue(storage: storage)
} }
/// Retrieve the value `Key` had on the last update. public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
///
/// Used to check if the value changed during the last update.
func previousValue<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
where Key: PreferenceKey where Key: PreferenceKey
{ {
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key)) let previousValues = self.value(forKey: key).valueList
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
} }
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self) public mutating func merge(with other: Self) {
where Key: PreferenceKey self = merging(with: other)
{
let keyID = ObjectIdentifier(key)
if !values.keys.contains(keyID) {
values[keyID] = .init(key)
}
values[keyID]?.valueList.append(value)
parent?.insert(value, forKey: key)
} }
func merge(_ other: _PreferenceStore) { public func merging(with other: Self) -> Self {
for (key, otherStorage) in other.values { var result = values
if let storage = values[key] { for (key, value) in other.values {
storage.merge(otherStorage) result[key] = value
} else {
values[key] = .init(valueList: otherStorage.valueList)
}
}
}
/// Copies `values` to `previousValues`, and clears `values`.
///
/// Each reconcile pass the preferences are collected from scratch, so we need to
/// clear out the old values.
func reset() {
previousValues = values.mapValues {
_PreferenceValueStorage(valueList: $0.valueList)
}
for storage in values.values {
storage.reset()
} }
return .init(values: result)
} }
} }

View File

@ -25,26 +25,12 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView { public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
let value = preferenceStore.value(forKey: Key.self) let value = preferenceStore.value(forKey: Key.self)
let previousValue = value.reduce((value.storage.valueList as? [Key.Value] ?? []).dropLast()) let previousValue = value.reduce(value.valueList.dropLast())
if previousValue != value.value { if previousValue != value.value {
action(value.value) action(value.value)
} }
return content.view return content.view
} }
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: {
let value = $0.value(forKey: Key.self).value
let previousValue = $0.previousValue(forKey: Key.self).value
if value != previousValue {
inputs.content.action(value)
}
}
)
}
} }
public extension View { public extension View {

View File

@ -20,12 +20,11 @@
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
where Key: PreferenceKey, Content: View where Key: PreferenceKey, Content: View
{ {
@State @State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self)) valueList: [Key.defaultValue]
)
public let transform: (_PreferenceValue<Key>) -> Content public let transform: (_PreferenceValue<Key>) -> Content
private var valueReference: _PreferenceValue<Key>?
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) { public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
self.transform = transform self.transform = transform
} }
@ -35,18 +34,7 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
} }
public var body: some View { public var body: some View {
transform(valueReference ?? resolvedValue) transform(resolvedValue)
}
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
let preferenceStore = inputs.preferenceStore ?? .init()
inputs.updateContent {
$0.valueReference = preferenceStore.value(forKey: Key.self)
}
return .init(
inputs: inputs,
preferenceStore: preferenceStore
)
} }
} }
@ -81,7 +69,7 @@ public extension View {
) -> some View ) -> some View
where Key: PreferenceKey, T: View where Key: PreferenceKey, T: View
{ {
Key._delay { self.overlay($0._force(transform)) } Key._delay { self.overlay(transform($0.value)) }
} }
func backgroundPreferenceValue<Key, T>( func backgroundPreferenceValue<Key, T>(
@ -90,6 +78,6 @@ public extension View {
) -> some View ) -> some View
where Key: PreferenceKey, T: View where Key: PreferenceKey, T: View
{ {
Key._delay { self.background($0._force(transform)) } Key._delay { self.background(transform($0.value)) }
} }
} }

View File

@ -34,18 +34,6 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
preferenceStore.insert(newValue, forKey: Key.self) preferenceStore.insert(newValue, forKey: Key.self)
return content.view return content.view
} }
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: {
var value = $0.value(forKey: Key.self).value
inputs.content.transform(&value)
$0.insert(value, forKey: Key.self)
}
)
}
} }
public extension View { public extension View {

View File

@ -27,14 +27,6 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
preferenceStore.insert(value, forKey: Key.self) preferenceStore.insert(value, forKey: Key.self)
return content.view return content.view
} }
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: { $0.insert(inputs.content.value, forKey: Key.self) }
)
}
} }
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable { extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {

View File

@ -18,8 +18,7 @@ import Foundation
public protocol PreviewProvider { public protocol PreviewProvider {
associatedtype Previews: View associatedtype Previews: View
@ViewBuilder @ViewBuilder static var previews: Previews { get }
static var previews: Previews { get }
} }
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral { public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {

View File

@ -20,8 +20,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
typealias FieldTypeAccessor = @convention(c) typealias FieldTypeAccessor = @convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
(UnsafePointer<Int>) -> UnsafePointer<Int>
struct StructTypeDescriptor { struct StructTypeDescriptor {
let flags: Int32 let flags: Int32

View File

@ -20,21 +20,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
public struct PropertyInfo: Hashable { public struct PropertyInfo {
// Hashable/Equatable conformance is not synthesize for metatypes.
public static func == (lhs: PropertyInfo, rhs: PropertyInfo) -> Bool {
lhs.name == rhs.name && lhs.type == rhs.type && lhs.isVar == rhs.isVar && lhs.offset == rhs
.offset && lhs.ownerType == rhs.ownerType
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(ObjectIdentifier(type))
hasher.combine(isVar)
hasher.combine(offset)
hasher.combine(ObjectIdentifier(ownerType))
}
public let name: String public let name: String
public let type: Any.Type public let type: Any.Type
public let isVar: Bool public let isVar: Bool

View File

@ -24,9 +24,7 @@ struct MetadataOffset<Pointee> {
let offset: Int32 let offset: Int32
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> { func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
// Data pointers in constant metadata are absolute until SwiftWasm 5.6 #if arch(wasm32)
// Since SwiftWasm 5.7, they are relative as well as other platforms.
#if arch(wasm32) && !compiler(>=5.7)
return UnsafePointer<Pointee>(bitPattern: Int(offset))! return UnsafePointer<Pointee>(bitPattern: Int(offset))!
#else #else
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self) return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)

View File

@ -1,76 +0,0 @@
// Copyright 2022 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.
import Foundation
protocol AnyShapeBox {
var animatableDataBox: _AnyAnimatableData { get set }
func path(in rect: CGRect) -> Path
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}
private struct ConcreteAnyShapeBox<Base: Shape>: AnyShapeBox {
var base: Base
var animatableDataBox: _AnyAnimatableData {
get {
_AnyAnimatableData(base.animatableData)
}
set {
guard let newData = newValue.value as? Base.AnimatableData else {
// TODO: Should this crash?
return
}
base.animatableData = newData
}
}
func path(in rect: CGRect) -> Path {
base.path(in: rect)
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
base.sizeThatFits(proposal)
}
}
public struct AnyShape: Shape {
var box: AnyShapeBox
private init(_ box: AnyShapeBox) {
self.box = box
}
}
public extension AnyShape {
init<S: Shape>(_ shape: S) {
box = ConcreteAnyShapeBox(base: shape)
}
func path(in rect: CGRect) -> Path {
box.path(in: rect)
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
box.sizeThatFits(proposal)
}
var animatableData: _AnyAnimatableData {
get { box.animatableDataBox }
set { box.animatableDataBox = newValue }
}
}

View File

@ -38,8 +38,7 @@ extension ContainerRelativeShape: InsettableShape {
} }
@usableFromInline @usableFromInline
@frozen @frozen internal struct _Inset: InsettableShape, DynamicProperty {
internal struct _Inset: InsettableShape, DynamicProperty {
@usableFromInline @usableFromInline
internal var amount: CGFloat internal var amount: CGFloat
@inlinable @inlinable
@ -77,8 +76,7 @@ private extension EnvironmentValues {
} }
} }
@frozen @frozen public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
public var shape: Shape public var shape: Shape
@inlinable @inlinable
public init(shape: Shape) { self.shape = shape } public init(shape: Shape) { self.shape = shape }

View File

@ -18,9 +18,7 @@
import Foundation import Foundation
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape { public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
@Environment(\.self) @Environment(\.self) public var environment
public var environment
public var shape: S public var shape: S
public var style: StrokeStyle public var style: StrokeStyle

View File

@ -89,11 +89,8 @@ extension RoundedRectangle: InsettableShape {
@usableFromInline @usableFromInline
struct _Inset: InsettableShape { struct _Inset: InsettableShape {
@usableFromInline @usableFromInline var base: RoundedRectangle
var base: RoundedRectangle @usableFromInline var amount: CGFloat
@usableFromInline
var amount: CGFloat
@inlinable @inlinable
init(base: RoundedRectangle, amount: CGFloat) { init(base: RoundedRectangle, amount: CGFloat) {

View File

@ -21,18 +21,6 @@ public protocol Shape: Animatable, View {
func path(in rect: CGRect) -> Path func path(in rect: CGRect) -> Path
static var role: ShapeRole { get } static var role: ShapeRole { get }
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}
public extension Shape {
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
// TODO: Check if SwiftUI changes this behavior.
// SwiftUI seems to not compute the path at all and just return
// the following.
proposal.replacingUnspecifiedDimensions()
}
} }
public enum ShapeRole: Hashable { public enum ShapeRole: Hashable {
@ -66,15 +54,9 @@ public struct FillStyle: Equatable {
} }
} }
public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content: Shape, public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
Style: ShapeStyle @Environment(\.self) public var environment
{ @Environment(\.foregroundColor) public var foregroundColor
@Environment(\.self)
public var environment
@Environment(\.foregroundColor)
public var foregroundColor
public var shape: Content public var shape: Content
public var style: Style public var style: Style
public var fillStyle: FillStyle public var fillStyle: FillStyle
@ -84,32 +66,6 @@ public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content:
self.style = style self.style = style
self.fillStyle = fillStyle self.fillStyle = fillStyle
} }
public func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
.init()
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
for subview in subviews {
subview.place(
at: bounds.origin,
proposal: .init(width: bounds.width, height: bounds.height)
)
}
}
} }
public extension Shape { public extension Shape {

View File

@ -56,8 +56,7 @@ public extension View {
} }
} }
@frozen @frozen public struct _BackgroundStyleModifier<Style>: ViewModifier, EnvironmentModifier,
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
EnvironmentReader EnvironmentReader
where Style: ShapeStyle where Style: ShapeStyle
{ {

View File

@ -71,10 +71,9 @@ public extension View {
} }
} }
@frozen @frozen public struct _ForegroundStyleModifier<
public struct _ForegroundStyleModifier<
Primary, Secondary, Tertiary Primary, Secondary, Tertiary
>: ViewModifier, _EnvironmentModifier >: ViewModifier, EnvironmentModifier
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
{ {
public var primary: Primary public var primary: Primary

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct AngularGradient: ShapeStyle, View {
public struct AngularGradient: ShapeStyle, View {
internal var gradient: Gradient internal var gradient: Gradient
internal var center: UnitPoint internal var center: UnitPoint
internal var startAngle: Angle internal var startAngle: Angle

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct EllipticalGradient: ShapeStyle, View {
public struct EllipticalGradient: ShapeStyle, View {
internal var gradient: Gradient internal var gradient: Gradient
internal var center: UnitPoint internal var center: UnitPoint
internal var startRadiusFraction: CGFloat internal var startRadiusFraction: CGFloat

View File

@ -17,10 +17,8 @@
import Foundation import Foundation
@frozen @frozen public struct Gradient: Equatable {
public struct Gradient: Equatable { @frozen public struct Stop: Equatable {
@frozen
public struct Stop: Equatable {
public var color: Color public var color: Color
public var location: CGFloat public var location: CGFloat

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct LinearGradient: ShapeStyle, View {
public struct LinearGradient: ShapeStyle, View {
internal var gradient: Gradient internal var gradient: Gradient
internal var startPoint: UnitPoint internal var startPoint: UnitPoint
internal var endPoint: UnitPoint internal var endPoint: UnitPoint

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct RadialGradient: ShapeStyle, View {
public struct RadialGradient: ShapeStyle, View {
internal var gradient: Gradient internal var gradient: Gradient
internal var center: UnitPoint internal var center: UnitPoint
internal var startRadius: CGFloat internal var startRadius: CGFloat

View File

@ -16,8 +16,7 @@
// //
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles. /// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
@frozen @frozen public struct HierarchicalShapeStyle: ShapeStyle {
public struct HierarchicalShapeStyle: ShapeStyle {
@usableFromInline @usableFromInline
internal var id: UInt32 internal var id: UInt32

View File

@ -17,10 +17,6 @@
import Foundation import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
#endif
public struct StrokeStyle: Equatable { public struct StrokeStyle: Equatable {
public var lineWidth: CGFloat public var lineWidth: CGFloat
public var lineCap: CGLineCap public var lineCap: CGLineCap

View File

@ -58,12 +58,6 @@ public final class StackReconciler<R: Renderer> {
*/ */
public let rootTarget: R.TargetType public let rootTarget: R.TargetType
/** A root renderer's main preference store.
*/
public var preferenceStore: _PreferenceStore {
rootElement.preferenceStore
}
/** A root of the mounted elements tree to which all other mounted elements are attached to. /** A root of the mounted elements tree to which all other mounted elements are attached to.
*/ */
private let rootElement: MountedElement<R> private let rootElement: MountedElement<R>

View File

@ -23,8 +23,7 @@ protocol WritableValueStorage: ValueStorage {
var setter: ((Any, Transaction) -> ())? { get set } var setter: ((Any, Transaction) -> ())? { get set }
} }
@propertyWrapper @propertyWrapper public struct State<Value>: DynamicProperty {
public struct State<Value>: DynamicProperty {
private let initialValue: Value private let initialValue: Value
var anyInitialValue: Any { initialValue } var anyInitialValue: Any { initialValue }

View File

@ -103,7 +103,7 @@ public struct CGAffineTransform: Equatable {
/// Returns an affine transformation matrix constructed from a rotation value you provide. /// Returns an affine transformation matrix constructed from a rotation value you provide.
/// - Parameters: /// - Parameters:
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes. /// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
/// A positive value specifies clockwise rotation and a negative value specifies /// A positive value specifies clockwise rotation and anegative value specifies
/// counterclockwise rotation. /// counterclockwise rotation.
public init(rotationAngle angle: CGFloat) { public init(rotationAngle angle: CGFloat) {
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)

View File

@ -25,8 +25,7 @@
public struct ToggleStyleConfiguration { public struct ToggleStyleConfiguration {
public let label: AnyView public let label: AnyView
@Binding @Binding public var isOn: Swift.Bool
public var isOn: Swift.Bool
} }
public protocol ToggleStyle { public protocol ToggleStyle {

View File

@ -1,32 +0,0 @@
// Copyright 2022 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 6/20/22.
//
public enum LayoutDirection: Hashable, CaseIterable {
case leftToRight
case rightToLeft
}
extension EnvironmentValues {
private enum LayoutDirectionKey: EnvironmentKey {
static var defaultValue: LayoutDirection = .leftToRight
}
public var layoutDirection: LayoutDirection {
get { self[LayoutDirectionKey.self] }
set { self[LayoutDirectionKey.self] = newValue }
}
}

View File

@ -37,13 +37,11 @@ struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
case tagged(V) case tagged(V)
} }
@inlinable @inlinable static var defaultValue: Value { .untagged }
static var defaultValue: Value { .untagged }
} }
@usableFromInline @usableFromInline
struct IsAuxiliaryContentTraitKey: _ViewTraitKey { struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
@inlinable @inlinable static var defaultValue: Bool { false }
static var defaultValue: Bool { false }
@usableFromInline typealias Value = Bool @usableFromInline typealias Value = Bool
} }

View File

@ -17,8 +17,7 @@
import Foundation import Foundation
@frozen @frozen public struct AnyTransition {
public struct AnyTransition {
fileprivate let box: _AnyTransitionBox fileprivate let box: _AnyTransitionBox
private init(_ box: _AnyTransitionBox) { private init(_ box: _AnyTransitionBox) {
@ -28,16 +27,14 @@ public struct AnyTransition {
@usableFromInline @usableFromInline
struct TransitionTraitKey: _ViewTraitKey { struct TransitionTraitKey: _ViewTraitKey {
@inlinable @inlinable static var defaultValue: AnyTransition { .opacity }
static var defaultValue: AnyTransition { .opacity }
@usableFromInline typealias Value = AnyTransition @usableFromInline typealias Value = AnyTransition
} }
@usableFromInline @usableFromInline
struct CanTransitionTraitKey: _ViewTraitKey { struct CanTransitionTraitKey: _ViewTraitKey {
@inlinable @inlinable static var defaultValue: Bool { false }
static var defaultValue: Bool { false }
@usableFromInline typealias Value = Bool @usableFromInline typealias Value = Bool
} }

View File

@ -24,8 +24,7 @@ public protocol _TraitWritingModifierProtocol {
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
} }
@frozen @frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
where Trait: _ViewTraitKey where Trait: _ViewTraitKey
{ {
public let value: Trait.Value public let value: Trait.Value
@ -41,14 +40,6 @@ public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierP
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) { public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
viewTraitStore.insert(value, forKey: Trait.self) viewTraitStore.insert(value, forKey: Trait.self)
} }
public static func _makeView(_ inputs: ViewInputs<_TraitWritingModifier<Trait>>)
-> ViewOutputs
{
var store = inputs.traits ?? .init()
store.insert(inputs.content.value, forKey: Trait.self)
return .init(inputs: inputs, traits: store)
}
} }
extension ModifiedContent: _TraitWritingModifierProtocol extension ModifiedContent: _TraitWritingModifierProtocol

View File

@ -16,21 +16,21 @@
// //
public struct _ViewTraitStore { public struct _ViewTraitStore {
public var values = [ObjectIdentifier: Any]() public var values = [String: Any]()
public init(values: [ObjectIdentifier: Any] = [:]) { public init(values: [String: Any] = [:]) {
self.values = values self.values = values
} }
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
where Key: _ViewTraitKey where Key: _ViewTraitKey
{ {
values[ObjectIdentifier(key)] as? Key.Value ?? Key.defaultValue values[String(reflecting: key)] as? Key.Value ?? Key.defaultValue
} }
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self) public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
where Key: _ViewTraitKey where Key: _ViewTraitKey
{ {
values[ObjectIdentifier(key)] = value values[String(reflecting: key)] = value
} }
} }

View File

@ -40,8 +40,6 @@ public struct AnyView: _PrimitiveView {
*/ */
let bodyType: Any.Type let bodyType: Any.Type
let visitChildren: (ViewVisitor, Any) -> ()
public init<V>(_ view: V) where V: View { public init<V>(_ view: V) where V: View {
if let anyView = view as? AnyView { if let anyView = view as? AnyView {
self = anyView self = anyView
@ -54,14 +52,8 @@ public struct AnyView: _PrimitiveView {
self.view = view self.view = view
// swiftlint:disable:next force_cast // swiftlint:disable:next force_cast
bodyClosure = { AnyView(($0 as! V).body) } bodyClosure = { AnyView(($0 as! V).body) }
// swiftlint:disable:next force_cast
visitChildren = { $0.visit($1 as! V) }
} }
} }
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitChildren(visitor, view)
}
} }
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? { public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {

Some files were not shown because too many files have changed in this diff Show More