Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
0ccb84afd5 |
|
@ -3,7 +3,7 @@ name: Bug report
|
|||
about: Create a report to help us improve
|
||||
title: ''
|
||||
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.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. macOS 12.4]
|
||||
- OS: [e.g. macOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- 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):**
|
||||
- Device: [e.g. iPhone 6]
|
||||
- OS: [e.g. iOS15.1]
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- 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**
|
||||
Add any other context about the problem here.
|
||||
|
|
|
@ -14,88 +14,76 @@ jobs:
|
|||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
with:
|
||||
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
|
||||
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:
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo "${{ matrix.toolchain }}" > .swift-version
|
||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
with:
|
||||
shell-action: carton test --environment node
|
||||
shell-action: carton test
|
||||
|
||||
core_macos_build:
|
||||
runs-on: macos-12
|
||||
# Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
|
||||
# core_macos_build:
|
||||
# runs-on: macos-11
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run the test suite on macOS, build the demo project for iOS
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
|
||||
# avoid building unrelated products for testing by specifying the test product explicitly
|
||||
swift build --product TokamakPackageTests
|
||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Run the test suite on macOS, build the demo project for iOS
|
||||
# shell: bash
|
||||
# run: |
|
||||
# set -ex
|
||||
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
||||
# # avoid building unrelated products for testing by specifying the test product explicitly
|
||||
# swift build --product TokamakPackageTests
|
||||
# `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||
# (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.
|
||||
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
xcpretty --color
|
||||
# # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||
# # Disable macOS builds until Monterey is available on GHA.
|
||||
# # xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||
# # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
# # xcpretty --color
|
||||
|
||||
cd "NativeDemo"
|
||||
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
xcpretty --color
|
||||
cd ..
|
||||
# cd "NativeDemo"
|
||||
# xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||
# CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
# xcpretty --color
|
||||
# cd ..
|
||||
|
||||
./benchmark.sh
|
||||
# ./benchmark.sh
|
||||
|
||||
- name: Upload failed snapshots
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: Failed snapshots
|
||||
path: '*Tests'
|
||||
# - name: Upload failed snapshots
|
||||
# uses: actions/upload-artifact@v2
|
||||
# if: ${{ failure() }}
|
||||
# with:
|
||||
# name: Failed snapshots
|
||||
# path: '*Tests'
|
||||
|
||||
# FIXME: disabled due to build errors, to be investigated
|
||||
# gtk_macos_build:
|
||||
# runs-on: macos-12
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Build the GTK renderer on macOS
|
||||
# shell: bash
|
||||
# run: |
|
||||
# set -ex
|
||||
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
|
||||
#
|
||||
# brew install gtk+3
|
||||
#
|
||||
# make build
|
||||
# gtk_macos_build:
|
||||
# runs-on: macos-11
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Build the GTK renderer on macOS
|
||||
# shell: bash
|
||||
# run: |
|
||||
# set -ex
|
||||
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
||||
|
||||
# brew install gtk+3
|
||||
|
||||
# make build
|
||||
|
||||
gtk_ubuntu_18_04_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-bionic
|
||||
image: swiftlang/swift:nightly-bionic
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -110,7 +98,7 @@ jobs:
|
|||
gtk_ubuntu_20_04_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
image: swiftlang/swift:nightly-focal
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -8,26 +8,26 @@ on:
|
|||
jobs:
|
||||
codecov:
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
image: swiftlang/swift:nightly-focal
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Test Target
|
||||
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||
- name: Run Tests
|
||||
run: swift test --enable-code-coverage --skip-build
|
||||
- name: Generate Branch Coverage Report
|
||||
uses: mattpolzin/swift-codecov-action@0.7.1
|
||||
id: cov
|
||||
with:
|
||||
MINIMUM_COVERAGE: 15
|
||||
- name: Post Positive Results
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- name: Post Negative Results
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Test Target
|
||||
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||
- name: Run Tests
|
||||
run: swift test --enable-code-coverage --skip-build
|
||||
- name: Generate Branch Coverage Report
|
||||
uses: mattpolzin/swift-codecov-action@0.7.1
|
||||
id: cov
|
||||
with:
|
||||
MINIMUM_COVERAGE: 15
|
||||
- name: Post Positive Results
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- name: Post Negative Results
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
|
|
|
@ -24,7 +24,6 @@ jobs:
|
|||
dependencies,
|
||||
documentation,
|
||||
enhancement,
|
||||
Fiber,
|
||||
refactor,
|
||||
SwiftUI compatibility,
|
||||
test suite,
|
||||
|
|
|
@ -8,7 +8,5 @@
|
|||
--maxwidth 100
|
||||
--wraparguments before-first
|
||||
--funcattributes prev-line
|
||||
--typeattributes prev-line
|
||||
--varattributes prev-line
|
||||
--disable andOperator
|
||||
--swiftversion 5.6
|
||||
--swiftversion 5.4
|
||||
|
|
|
@ -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
|
||||
`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
|
||||
|
||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
||||
|
|
|
@ -46,23 +46,23 @@ let package = Package(
|
|||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
|
||||
url: "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
from: "0.15.0"
|
||||
),
|
||||
.package(
|
||||
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
|
||||
url: "https://github.com/OpenCombine/OpenCombine.git",
|
||||
from: "0.12.0"
|
||||
),
|
||||
.package(
|
||||
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
|
||||
url: "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||
from: "0.2.0"
|
||||
),
|
||||
.package(
|
||||
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
|
||||
url: "https://github.com/google/swift-benchmark",
|
||||
from: "0.1.2"
|
||||
),
|
||||
.package(
|
||||
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
|
||||
url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
||||
from: "1.9.0"
|
||||
),
|
||||
],
|
||||
|
@ -135,7 +135,6 @@ let package = Package(
|
|||
dependencies: [
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
|
@ -195,25 +194,6 @@ let package = Package(
|
|||
name: "TokamakTestRenderer",
|
||||
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(
|
||||
name: "TokamakTests",
|
||||
dependencies: ["TokamakTestRenderer"]
|
||||
|
|
37
README.md
37
README.md
|
@ -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
|
||||
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
|
||||
|
||||
### For app developers
|
||||
|
@ -172,7 +147,7 @@ struct CounterApp: App {
|
|||
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.
|
||||
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
|
||||
|
||||
|
@ -241,9 +216,6 @@ carton dev
|
|||
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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
||||
[Ezra Berch](https://github.com/ezraberch),
|
||||
[Jed Fox](https://jedfox.com),
|
||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
|
||||
[Yuta Saito](https://github.com/kateinoigakukun/).
|
||||
[David Hunt](https://github.com/foscomputerservices), [Ezra Berch](https://github.com/ezraberch),
|
||||
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
|
||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
|
|
@ -40,20 +40,14 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct EmptyAnimatableData: VectorArithmetic {
|
||||
@frozen public struct EmptyAnimatableData: VectorArithmetic {
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public static var zero: Self { .init() }
|
||||
|
||||
@inlinable public static var zero: Self { .init() }
|
||||
@inlinable
|
||||
public static func += (lhs: inout Self, rhs: Self) {}
|
||||
|
||||
@inlinable
|
||||
public static func -= (lhs: inout Self, rhs: Self) {}
|
||||
|
||||
@inlinable
|
||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||
.zero
|
||||
|
@ -66,15 +60,11 @@ public struct EmptyAnimatableData: VectorArithmetic {
|
|||
|
||||
@inlinable
|
||||
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 }
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||
@frozen public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||
where First: VectorArithmetic, Second: VectorArithmetic
|
||||
{
|
||||
public var first: First
|
||||
|
@ -91,8 +81,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
|
|||
set { (first, second) = newValue }
|
||||
}
|
||||
|
||||
@_transparent
|
||||
public static var zero: Self {
|
||||
@_transparent public static var zero: Self {
|
||||
@_transparent get {
|
||||
.init(First.zero, Second.zero)
|
||||
}
|
||||
|
@ -126,8 +115,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
|
|||
second.scale(by: rhs)
|
||||
}
|
||||
|
||||
@_transparent
|
||||
public var magnitudeSquared: Double {
|
||||
@_transparent public var magnitudeSquared: Double {
|
||||
@_transparent get {
|
||||
first.magnitudeSquared + second.magnitudeSquared
|
||||
}
|
||||
|
|
|
@ -139,8 +139,7 @@ public struct _AnimationProxy {
|
|||
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||
@frozen public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||
where Value: Equatable
|
||||
{
|
||||
public var animation: Animation?
|
||||
|
@ -156,9 +155,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
|||
let content: Content
|
||||
let animation: Animation?
|
||||
let value: Value
|
||||
|
||||
@State
|
||||
private var lastValue: Value?
|
||||
@State private var lastValue: Value?
|
||||
|
||||
var body: some View {
|
||||
content.transaction {
|
||||
|
@ -183,8 +180,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _AnimationView<Content>: View
|
||||
@frozen public struct _AnimationView<Content>: View
|
||||
where Content: Equatable, Content: View
|
||||
{
|
||||
public var content: Content
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
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.
|
||||
static var _active: Self?
|
||||
|
||||
|
@ -50,8 +50,7 @@ protocol _TransactionModifierProtocol {
|
|||
func modifyTransaction(_ transaction: inout Transaction)
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _TransactionModifier: ViewModifier {
|
||||
@frozen public struct _TransactionModifier: ViewModifier {
|
||||
public var transform: (inout Transaction) -> ()
|
||||
|
||||
@inlinable
|
||||
|
@ -78,8 +77,7 @@ extension ModifiedContent: _TransactionModifierProtocol
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
|
||||
@frozen public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
|
||||
public var content: V
|
||||
public var base: _TransactionModifier
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ public protocol VectorArithmetic: AdditiveArithmetic {
|
|||
extension Float: VectorArithmetic {
|
||||
@_transparent
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
@ -35,9 +33,7 @@ extension Float: VectorArithmetic {
|
|||
extension Double: VectorArithmetic {
|
||||
@_transparent
|
||||
public mutating func scale(by rhs: Double) { self *= rhs }
|
||||
|
||||
@_transparent
|
||||
public var magnitudeSquared: Double {
|
||||
@_transparent public var magnitudeSquared: Double {
|
||||
@_transparent get { self * self }
|
||||
}
|
||||
}
|
||||
|
@ -45,9 +41,7 @@ extension Double: VectorArithmetic {
|
|||
extension CGFloat: VectorArithmetic {
|
||||
@_transparent
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import Foundation
|
|||
public protocol _AnimationSolver {
|
||||
/// Solve value at a specific point in time.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?? ()
|
||||
}
|
||||
}
|
|
@ -20,8 +20,7 @@ import Foundation
|
|||
public protocol _VectorMath: Animatable {}
|
||||
|
||||
public extension _VectorMath {
|
||||
@inlinable
|
||||
var magnitude: Double {
|
||||
@inlinable var magnitude: Double {
|
||||
animatableData.magnitudeSquared.squareRoot()
|
||||
}
|
||||
|
||||
|
|
|
@ -28,10 +28,7 @@ public protocol App: _TitledApp {
|
|||
var body: Body { get }
|
||||
|
||||
/// Implemented by the renderer to mount the `App`
|
||||
static func _launch(
|
||||
_ app: Self,
|
||||
with configuration: _AppConfiguration
|
||||
)
|
||||
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
|
||||
|
||||
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
||||
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
|
||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||
|
||||
static var _configuration: _AppConfiguration { get }
|
||||
|
||||
static func main()
|
||||
|
||||
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 {
|
||||
static var _configuration: _AppConfiguration { .init() }
|
||||
|
||||
static func main() {
|
||||
let app = Self()
|
||||
_launch(app, with: Self._configuration)
|
||||
_launch(app, EnvironmentValues())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,9 @@
|
|||
|
||||
import OpenCombineShim
|
||||
|
||||
@propertyWrapper
|
||||
public struct AppStorage<Value>: DynamicProperty {
|
||||
@propertyWrapper public struct AppStorage<Value>: DynamicProperty {
|
||||
let provider: _StorageProvider?
|
||||
|
||||
@Environment(\._defaultAppStorage)
|
||||
var defaultProvider: _StorageProvider?
|
||||
|
||||
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
|
||||
var unwrappedProvider: _StorageProvider {
|
||||
provider ?? defaultProvider!
|
||||
}
|
||||
|
|
|
@ -21,23 +21,8 @@ public protocol Scene {
|
|||
// FIXME: If I put `@SceneBuilder` in front of this
|
||||
// it fails to build with no useful error message.
|
||||
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 {
|
||||
var title: Text? { get }
|
||||
}
|
||||
|
|
|
@ -29,14 +29,7 @@ public extension SceneBuilder {
|
|||
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
||||
C1: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1),
|
||||
children: [_AnyScene(c0), _AnyScene(c1)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
}
|
||||
)
|
||||
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,15 +37,7 @@ public extension SceneBuilder {
|
|||
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
||||
where C0: Scene, C1: Scene, C2: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
}
|
||||
)
|
||||
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,13 +50,7 @@ public extension SceneBuilder {
|
|||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
}
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -86,14 +65,7 @@ public extension SceneBuilder {
|
|||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, 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)
|
||||
}
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -118,15 +90,7 @@ public extension SceneBuilder {
|
|||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_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(c5),
|
||||
_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(c6),
|
||||
_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(c7),
|
||||
_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(c8),
|
||||
_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)
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,7 @@ public enum _DefaultSceneStorageProvider {
|
|||
public static var `default`: _StorageProvider!
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct SceneStorage<Value>: DynamicProperty {
|
||||
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
|
||||
let key: String
|
||||
let defaultValue: Value
|
||||
let store: (_StorageProvider, String, Value) -> ()
|
||||
|
|
|
@ -75,8 +75,4 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
// public init(_ titleKey: LocalizedStringKey,
|
||||
// @ViewBuilder content: () -> Content) {
|
||||
// }
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ public struct _AnyApp: App {
|
|||
}
|
||||
|
||||
@_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.")
|
||||
}
|
||||
|
||||
|
@ -51,10 +51,6 @@ public struct _AnyApp: App {
|
|||
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)
|
||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
||||
|
|
|
@ -17,17 +17,11 @@
|
|||
|
||||
struct _TupleScene<T>: Scene, GroupScene {
|
||||
let value: T
|
||||
let children: [_AnyScene]
|
||||
let visit: (SceneVisitor) -> ()
|
||||
var children: [_AnyScene]
|
||||
|
||||
init(
|
||||
_ value: T,
|
||||
children: [_AnyScene],
|
||||
visit: @escaping (SceneVisitor) -> ()
|
||||
) {
|
||||
init(_ value: T, children: [_AnyScene]) {
|
||||
self.value = value
|
||||
self.children = children
|
||||
self.visit = visit
|
||||
}
|
||||
|
||||
var body: Never {
|
||||
|
|
|
@ -23,8 +23,7 @@ protocol EnvironmentReader {
|
|||
mutating func setContent(from values: EnvironmentValues)
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct Environment<Value>: DynamicProperty {
|
||||
@propertyWrapper public struct Environment<Value>: DynamicProperty {
|
||||
enum Content {
|
||||
case keyPath(KeyPath<EnvironmentValues, Value>)
|
||||
case value(Value)
|
||||
|
|
|
@ -17,24 +17,11 @@ public protocol EnvironmentKey {
|
|||
static var defaultValue: Value { get }
|
||||
}
|
||||
|
||||
/// This protocol defines a type which mutates the environment in some way.
|
||||
/// Unlike `EnvironmentalModifier`, which reads the environment to
|
||||
/// create a `ViewModifier`.
|
||||
///
|
||||
/// It can be applied to a `View` or `ViewModifier`.
|
||||
public protocol _EnvironmentModifier {
|
||||
protocol EnvironmentModifier {
|
||||
func modifyEnvironment(_ values: inout EnvironmentValues)
|
||||
}
|
||||
|
||||
public extension ViewModifier where Self: _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 struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
|
||||
public let keyPath: WritableKeyPath<EnvironmentValues, Value>
|
||||
public let value: Value
|
||||
|
||||
|
@ -45,7 +32,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentM
|
|||
|
||||
public typealias Body = Never
|
||||
|
||||
public func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
values[keyPath: keyPath] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,10 @@
|
|||
|
||||
import OpenCombineShim
|
||||
|
||||
@propertyWrapper
|
||||
public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
where ObjectType: ObservableObject
|
||||
{
|
||||
@dynamicMemberLookup
|
||||
public struct Wrapper {
|
||||
@dynamicMemberLookup public struct Wrapper {
|
||||
internal let root: ObjectType
|
||||
public subscript<Subject>(
|
||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||
|
|
|
@ -76,7 +76,7 @@ public extension EnvironmentValues {
|
|||
}
|
||||
}
|
||||
|
||||
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
|
||||
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
||||
let environmentValues: EnvironmentValues
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) ?? "")
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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() }
|
||||
}
|
|
@ -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() }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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!
|
||||
}
|
||||
}
|
|
@ -14,8 +14,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public enum ContentMode: Hashable, CaseIterable {
|
||||
@frozen public enum ContentMode: Hashable, CaseIterable {
|
||||
case fit
|
||||
case fill
|
||||
}
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct _OffsetEffect: GeometryEffect, Equatable {
|
||||
@frozen public struct _OffsetEffect: GeometryEffect, Equatable {
|
||||
public var offset: CGSize
|
||||
|
||||
@inlinable
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||
@frozen public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||
public var scale: CGSize
|
||||
public var anchor: UnitPoint
|
||||
|
||||
|
|
|
@ -13,16 +13,14 @@
|
|||
// limitations under the License.
|
||||
|
||||
protocol ModifierContainer {
|
||||
var environmentModifier: _EnvironmentModifier? { get }
|
||||
var environmentModifier: EnvironmentModifier? { get }
|
||||
}
|
||||
|
||||
protocol ModifiedContentProtocol {}
|
||||
|
||||
/// A value with a modifier applied to it.
|
||||
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||
@Environment(\.self)
|
||||
public var environment
|
||||
|
||||
@Environment(\.self) public var environment
|
||||
public typealias Body = Never
|
||||
public private(set) var content: Content
|
||||
public private(set) var modifier: Modifier
|
||||
|
@ -34,7 +32,7 @@ public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
|||
}
|
||||
|
||||
extension ModifiedContent: ModifierContainer {
|
||||
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
|
||||
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
|
||||
}
|
||||
|
||||
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||
|
|
|
@ -24,18 +24,6 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
|||
public let content: Content
|
||||
public let background: Background
|
||||
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
|
||||
|
@ -89,8 +77,7 @@ public extension View {
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||
@frozen public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||
where Style: ShapeStyle, Bounds: Shape
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
@ -142,11 +129,6 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
|||
public let content: Content
|
||||
public let overlay: Overlay
|
||||
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
|
||||
|
|
|
@ -16,23 +16,6 @@ public protocol ViewModifier {
|
|||
typealias Content = _ViewModifier_Content<Self>
|
||||
associatedtype Body: View
|
||||
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
|
||||
|
@ -40,27 +23,15 @@ public struct _ViewModifier_Content<Modifier>: View
|
|||
{
|
||||
public let modifier: Modifier
|
||||
public let view: AnyView
|
||||
let visitChildren: (ViewVisitor) -> ()
|
||||
|
||||
public init(modifier: Modifier, view: AnyView) {
|
||||
self.modifier = modifier
|
||||
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 {
|
||||
view
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitChildren(visitor)
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
|
|
|
@ -78,6 +78,9 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
|
||||
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
||||
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
||||
if let parent = parent {
|
||||
parent.preferenceStore.merge(with: self.preferenceStore)
|
||||
}
|
||||
}
|
||||
|
||||
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
||||
|
|
|
@ -94,9 +94,13 @@ public class MountedElement<R: Renderer> {
|
|||
|
||||
public internal(set) var environmentValues: EnvironmentValues
|
||||
|
||||
private(set) weak var parent: MountedElement<R>?
|
||||
|
||||
var preferenceStore: _PreferenceStore = .init()
|
||||
weak var parent: MountedElement<R>?
|
||||
/// `didSet` on this field propagates the preference changes up the view tree.
|
||||
var preferenceStore: _PreferenceStore = .init() {
|
||||
didSet {
|
||||
parent?.preferenceStore.merge(with: preferenceStore)
|
||||
}
|
||||
}
|
||||
|
||||
public internal(set) var viewTraits: _ViewTraitStore
|
||||
|
||||
|
@ -106,7 +110,6 @@ public class MountedElement<R: Renderer> {
|
|||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
|
@ -115,7 +118,6 @@ public class MountedElement<R: Renderer> {
|
|||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
init(
|
||||
|
@ -129,7 +131,6 @@ public class MountedElement<R: Renderer> {
|
|||
self.environmentValues = environmentValues
|
||||
self.viewTraits = viewTraits
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
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.
|
||||
func prepareForMount(with transaction: Transaction) {
|
||||
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||
|
|
|
@ -25,41 +25,12 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
|
|||
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 {
|
||||
var storage: _PreferenceValueStorage
|
||||
|
||||
init(storage: _PreferenceValueStorage) {
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
/// Every value the `Key` has had.
|
||||
var valueList: [Key.Value]
|
||||
/// The latest value.
|
||||
public var value: Key.Value {
|
||||
reduce(storage.valueList.compactMap { $0 as? Key.Value })
|
||||
reduce(valueList)
|
||||
}
|
||||
|
||||
func reduce(_ values: [Key.Value]) -> Key.Value {
|
||||
|
@ -77,79 +48,38 @@ public extension _PreferenceValue {
|
|||
}
|
||||
}
|
||||
|
||||
public final class _PreferenceStore: CustomDebugStringConvertible {
|
||||
/// The values of the `_PreferenceStore` on the last update.
|
||||
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
|
||||
public struct _PreferenceStore {
|
||||
/// The backing values of the `_PreferenceStore`.
|
||||
private var values: [ObjectIdentifier: _PreferenceValueStorage]
|
||||
private var values: [String: Any]
|
||||
|
||||
weak var parent: _PreferenceStore?
|
||||
|
||||
public var debugDescription: String {
|
||||
"Preferences (\(ObjectIdentifier(self))): \(values)"
|
||||
}
|
||||
|
||||
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
|
||||
previousValues = [:]
|
||||
public init(values: [String: Any] = [:]) {
|
||||
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>
|
||||
where Key: PreferenceKey
|
||||
{
|
||||
let keyID = ObjectIdentifier(key)
|
||||
let storage: _PreferenceValueStorage
|
||||
if let existing = values[keyID] {
|
||||
storage = existing
|
||||
} else {
|
||||
storage = .init(key)
|
||||
values[keyID] = storage
|
||||
}
|
||||
return _PreferenceValue(storage: storage)
|
||||
values[String(reflecting: key)] as? _PreferenceValue<Key>
|
||||
?? _PreferenceValue(valueList: [Key.defaultValue])
|
||||
}
|
||||
|
||||
/// Retrieve the value `Key` had on the last update.
|
||||
///
|
||||
/// Used to check if the value changed during the last update.
|
||||
func previousValue<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
||||
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||
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)
|
||||
where Key: PreferenceKey
|
||||
{
|
||||
let keyID = ObjectIdentifier(key)
|
||||
if !values.keys.contains(keyID) {
|
||||
values[keyID] = .init(key)
|
||||
}
|
||||
values[keyID]?.valueList.append(value)
|
||||
parent?.insert(value, forKey: key)
|
||||
public mutating func merge(with other: Self) {
|
||||
self = merging(with: other)
|
||||
}
|
||||
|
||||
func merge(_ other: _PreferenceStore) {
|
||||
for (key, otherStorage) in other.values {
|
||||
if let storage = values[key] {
|
||||
storage.merge(otherStorage)
|
||||
} 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()
|
||||
public func merging(with other: Self) -> Self {
|
||||
var result = values
|
||||
for (key, value) in other.values {
|
||||
result[key] = value
|
||||
}
|
||||
return .init(values: result)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,26 +25,12 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
|
|||
|
||||
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
||||
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 {
|
||||
action(value.value)
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -20,12 +20,11 @@
|
|||
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
|
||||
where Key: PreferenceKey, Content: View
|
||||
{
|
||||
@State
|
||||
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self))
|
||||
@State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
|
||||
valueList: [Key.defaultValue]
|
||||
)
|
||||
public let transform: (_PreferenceValue<Key>) -> Content
|
||||
|
||||
private var valueReference: _PreferenceValue<Key>?
|
||||
|
||||
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
||||
self.transform = transform
|
||||
}
|
||||
|
@ -35,18 +34,7 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
|
|||
}
|
||||
|
||||
public var body: some View {
|
||||
transform(valueReference ?? 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
|
||||
)
|
||||
transform(resolvedValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +69,7 @@ public extension View {
|
|||
) -> some View
|
||||
where Key: PreferenceKey, T: View
|
||||
{
|
||||
Key._delay { self.overlay($0._force(transform)) }
|
||||
Key._delay { self.overlay(transform($0.value)) }
|
||||
}
|
||||
|
||||
func backgroundPreferenceValue<Key, T>(
|
||||
|
@ -90,6 +78,6 @@ public extension View {
|
|||
) -> some View
|
||||
where Key: PreferenceKey, T: View
|
||||
{
|
||||
Key._delay { self.background($0._force(transform)) }
|
||||
Key._delay { self.background(transform($0.value)) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,18 +34,6 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
|
|||
preferenceStore.insert(newValue, forKey: Key.self)
|
||||
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 {
|
||||
|
|
|
@ -27,14 +27,6 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
|
|||
preferenceStore.insert(value, forKey: Key.self)
|
||||
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 {
|
||||
|
|
|
@ -18,8 +18,7 @@ import Foundation
|
|||
public protocol PreviewProvider {
|
||||
associatedtype Previews: View
|
||||
|
||||
@ViewBuilder
|
||||
static var previews: Previews { get }
|
||||
@ViewBuilder static var previews: Previews { get }
|
||||
}
|
||||
|
||||
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
typealias FieldTypeAccessor = @convention(c)
|
||||
(UnsafePointer<Int>) -> UnsafePointer<Int>
|
||||
typealias FieldTypeAccessor = @convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
|
||||
|
||||
struct StructTypeDescriptor {
|
||||
let flags: Int32
|
||||
|
|
|
@ -20,21 +20,7 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
public struct PropertyInfo: Hashable {
|
||||
// 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 struct PropertyInfo {
|
||||
public let name: String
|
||||
public let type: Any.Type
|
||||
public let isVar: Bool
|
||||
|
|
|
@ -24,9 +24,7 @@ struct MetadataOffset<Pointee> {
|
|||
let offset: Int32
|
||||
|
||||
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
|
||||
// Data pointers in constant metadata are absolute until SwiftWasm 5.6
|
||||
// Since SwiftWasm 5.7, they are relative as well as other platforms.
|
||||
#if arch(wasm32) && !compiler(>=5.7)
|
||||
#if arch(wasm32)
|
||||
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
|
||||
#else
|
||||
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -38,8 +38,7 @@ extension ContainerRelativeShape: InsettableShape {
|
|||
}
|
||||
|
||||
@usableFromInline
|
||||
@frozen
|
||||
internal struct _Inset: InsettableShape, DynamicProperty {
|
||||
@frozen internal struct _Inset: InsettableShape, DynamicProperty {
|
||||
@usableFromInline
|
||||
internal var amount: CGFloat
|
||||
@inlinable
|
||||
|
@ -77,8 +76,7 @@ private extension EnvironmentValues {
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
|
||||
@frozen public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
|
||||
public var shape: Shape
|
||||
@inlinable
|
||||
public init(shape: Shape) { self.shape = shape }
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
import Foundation
|
||||
|
||||
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
|
||||
@Environment(\.self)
|
||||
public var environment
|
||||
|
||||
@Environment(\.self) public var environment
|
||||
public var shape: S
|
||||
public var style: StrokeStyle
|
||||
|
||||
|
|
|
@ -89,11 +89,8 @@ extension RoundedRectangle: InsettableShape {
|
|||
|
||||
@usableFromInline
|
||||
struct _Inset: InsettableShape {
|
||||
@usableFromInline
|
||||
var base: RoundedRectangle
|
||||
|
||||
@usableFromInline
|
||||
var amount: CGFloat
|
||||
@usableFromInline var base: RoundedRectangle
|
||||
@usableFromInline var amount: CGFloat
|
||||
|
||||
@inlinable
|
||||
init(base: RoundedRectangle, amount: CGFloat) {
|
||||
|
|
|
@ -21,18 +21,6 @@ public protocol Shape: Animatable, View {
|
|||
func path(in rect: CGRect) -> Path
|
||||
|
||||
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 {
|
||||
|
@ -66,15 +54,9 @@ public struct FillStyle: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content: Shape,
|
||||
Style: ShapeStyle
|
||||
{
|
||||
@Environment(\.self)
|
||||
public var environment
|
||||
|
||||
@Environment(\.foregroundColor)
|
||||
public var foregroundColor
|
||||
|
||||
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
|
||||
@Environment(\.self) public var environment
|
||||
@Environment(\.foregroundColor) public var foregroundColor
|
||||
public var shape: Content
|
||||
public var style: Style
|
||||
public var fillStyle: FillStyle
|
||||
|
@ -84,32 +66,6 @@ public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content:
|
|||
self.style = style
|
||||
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 {
|
||||
|
|
|
@ -56,8 +56,7 @@ public extension View {
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
|
||||
@frozen public struct _BackgroundStyleModifier<Style>: ViewModifier, EnvironmentModifier,
|
||||
EnvironmentReader
|
||||
where Style: ShapeStyle
|
||||
{
|
||||
|
|
|
@ -71,10 +71,9 @@ public extension View {
|
|||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _ForegroundStyleModifier<
|
||||
@frozen public struct _ForegroundStyleModifier<
|
||||
Primary, Secondary, Tertiary
|
||||
>: ViewModifier, _EnvironmentModifier
|
||||
>: ViewModifier, EnvironmentModifier
|
||||
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
|
||||
{
|
||||
public var primary: Primary
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct AngularGradient: ShapeStyle, View {
|
||||
@frozen public struct AngularGradient: ShapeStyle, View {
|
||||
internal var gradient: Gradient
|
||||
internal var center: UnitPoint
|
||||
internal var startAngle: Angle
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct EllipticalGradient: ShapeStyle, View {
|
||||
@frozen public struct EllipticalGradient: ShapeStyle, View {
|
||||
internal var gradient: Gradient
|
||||
internal var center: UnitPoint
|
||||
internal var startRadiusFraction: CGFloat
|
||||
|
|
|
@ -17,10 +17,8 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct Gradient: Equatable {
|
||||
@frozen
|
||||
public struct Stop: Equatable {
|
||||
@frozen public struct Gradient: Equatable {
|
||||
@frozen public struct Stop: Equatable {
|
||||
public var color: Color
|
||||
public var location: CGFloat
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct LinearGradient: ShapeStyle, View {
|
||||
@frozen public struct LinearGradient: ShapeStyle, View {
|
||||
internal var gradient: Gradient
|
||||
internal var startPoint: UnitPoint
|
||||
internal var endPoint: UnitPoint
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct RadialGradient: ShapeStyle, View {
|
||||
@frozen public struct RadialGradient: ShapeStyle, View {
|
||||
internal var gradient: Gradient
|
||||
internal var center: UnitPoint
|
||||
internal var startRadius: CGFloat
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
//
|
||||
|
||||
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
|
||||
@frozen
|
||||
public struct HierarchicalShapeStyle: ShapeStyle {
|
||||
@frozen public struct HierarchicalShapeStyle: ShapeStyle {
|
||||
@usableFromInline
|
||||
internal var id: UInt32
|
||||
|
||||
|
|
|
@ -17,10 +17,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
#endif
|
||||
|
||||
public struct StrokeStyle: Equatable {
|
||||
public var lineWidth: CGFloat
|
||||
public var lineCap: CGLineCap
|
||||
|
|
|
@ -58,12 +58,6 @@ public final class StackReconciler<R: Renderer> {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
private let rootElement: MountedElement<R>
|
||||
|
|
|
@ -23,8 +23,7 @@ protocol WritableValueStorage: ValueStorage {
|
|||
var setter: ((Any, Transaction) -> ())? { get set }
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct State<Value>: DynamicProperty {
|
||||
@propertyWrapper public struct State<Value>: DynamicProperty {
|
||||
private let initialValue: Value
|
||||
|
||||
var anyInitialValue: Any { initialValue }
|
||||
|
|
|
@ -103,7 +103,7 @@ public struct CGAffineTransform: Equatable {
|
|||
/// Returns an affine transformation matrix constructed from a rotation value you provide.
|
||||
/// - Parameters:
|
||||
/// - 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.
|
||||
public init(rotationAngle angle: CGFloat) {
|
||||
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||
|
|
|
@ -25,8 +25,7 @@
|
|||
|
||||
public struct ToggleStyleConfiguration {
|
||||
public let label: AnyView
|
||||
@Binding
|
||||
public var isOn: Swift.Bool
|
||||
@Binding public var isOn: Swift.Bool
|
||||
}
|
||||
|
||||
public protocol ToggleStyle {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -37,13 +37,11 @@ struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
|
|||
case tagged(V)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
static var defaultValue: Value { .untagged }
|
||||
@inlinable static var defaultValue: Value { .untagged }
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
|
||||
@inlinable
|
||||
static var defaultValue: Bool { false }
|
||||
@inlinable static var defaultValue: Bool { false }
|
||||
@usableFromInline typealias Value = Bool
|
||||
}
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct AnyTransition {
|
||||
@frozen public struct AnyTransition {
|
||||
fileprivate let box: _AnyTransitionBox
|
||||
|
||||
private init(_ box: _AnyTransitionBox) {
|
||||
|
@ -28,16 +27,14 @@ public struct AnyTransition {
|
|||
|
||||
@usableFromInline
|
||||
struct TransitionTraitKey: _ViewTraitKey {
|
||||
@inlinable
|
||||
static var defaultValue: AnyTransition { .opacity }
|
||||
@inlinable static var defaultValue: AnyTransition { .opacity }
|
||||
|
||||
@usableFromInline typealias Value = AnyTransition
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct CanTransitionTraitKey: _ViewTraitKey {
|
||||
@inlinable
|
||||
static var defaultValue: Bool { false }
|
||||
@inlinable static var defaultValue: Bool { false }
|
||||
|
||||
@usableFromInline typealias Value = Bool
|
||||
}
|
||||
|
|
|
@ -24,8 +24,7 @@ public protocol _TraitWritingModifierProtocol {
|
|||
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
||||
@frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
||||
where Trait: _ViewTraitKey
|
||||
{
|
||||
public let value: Trait.Value
|
||||
|
@ -41,14 +40,6 @@ public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierP
|
|||
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
||||
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
|
||||
|
|
|
@ -16,21 +16,21 @@
|
|||
//
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
||||
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)
|
||||
where Key: _ViewTraitKey
|
||||
{
|
||||
values[ObjectIdentifier(key)] = value
|
||||
values[String(reflecting: key)] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,6 @@ public struct AnyView: _PrimitiveView {
|
|||
*/
|
||||
let bodyType: Any.Type
|
||||
|
||||
let visitChildren: (ViewVisitor, Any) -> ()
|
||||
|
||||
public init<V>(_ view: V) where V: View {
|
||||
if let anyView = view as? AnyView {
|
||||
self = anyView
|
||||
|
@ -54,14 +52,8 @@ public struct AnyView: _PrimitiveView {
|
|||
self.view = view
|
||||
// swiftlint:disable:next force_cast
|
||||
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? {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue