Compare commits
33 Commits
Author | SHA1 | Date |
---|---|---|
|
eb50bca9f7 | |
|
cf1304ef90 | |
![]() |
e0d8e9db46 | |
![]() |
f1cbfcf073 | |
![]() |
56822c906b | |
![]() |
af810902bd | |
![]() |
b24b964fd7 | |
![]() |
e7e318e64e | |
![]() |
47c5a05068 | |
![]() |
fa0a8d2447 | |
![]() |
687baee97f | |
![]() |
10d9d32b97 | |
![]() |
4e8b84e4a1 | |
![]() |
676760d34b | |
![]() |
9d0e2fc067 | |
![]() |
d78ab20ea8 | |
![]() |
5e6fe2d2c9 | |
![]() |
c4717d5cae | |
![]() |
0b182d99a1 | |
![]() |
546b9e572f | |
![]() |
c935744ae8 | |
![]() |
6e2ccf71ea | |
![]() |
9db23c9e3f | |
![]() |
f4cd4955db | |
![]() |
03513dd5b3 | |
![]() |
355c880a1d | |
![]() |
c0c4534352 | |
![]() |
a604ef5269 | |
![]() |
3081f5521a | |
![]() |
2e7f561276 | |
![]() |
2ba548810c | |
![]() |
8177fc8cae | |
![]() |
a41ac37500 |
|
@ -3,7 +3,7 @@ name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: MaxDesiatov
|
assignees: carson-katri
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -28,17 +28,17 @@ A clear and concise description of what you expected to happen.
|
||||||
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. macOS]
|
- OS: [e.g. macOS 12.4]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
- Version of the browser [e.g. 22]
|
- Version of the browser [e.g. 22]
|
||||||
- Version of Tokamak [e.g. 0.6.1]
|
- Version of Tokamak [e.g. 0.10.1]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone 6]
|
- Device: [e.g. iPhone 6]
|
||||||
- OS: [e.g. iOS8.1]
|
- OS: [e.g. iOS15.1]
|
||||||
- Browser [e.g. stock browser, safari]
|
- Browser [e.g. stock browser, safari]
|
||||||
- Version of the browser [e.g. 22]
|
- Version of the browser [e.g. 22]
|
||||||
- Version of Tokamak [e.g. 0.6.1]
|
- Version of Tokamak [e.g. 0.10.1]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
|
@ -14,76 +14,88 @@ jobs:
|
||||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||||
with:
|
with:
|
||||||
shell-action: carton bundle --product TokamakDemo
|
shell-action: carton bundle --product TokamakDemo
|
||||||
|
- name: Check binary size
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ls -la Bundle
|
||||||
|
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
|
||||||
|
|
||||||
swiftwasm_test_5_6:
|
swiftwasm_test:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { toolchain: wasm-5.6.0-RELEASE }
|
||||||
|
- { toolchain: wasm-5.7-SNAPSHOT-2022-07-27-a }
|
||||||
|
- { toolchain: wasm-DEVELOPMENT-SNAPSHOT-2022-07-23-a }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- run: echo "${{ matrix.toolchain }}" > .swift-version
|
||||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||||
with:
|
with:
|
||||||
shell-action: carton test
|
shell-action: carton test --environment node
|
||||||
|
|
||||||
# Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
|
core_macos_build:
|
||||||
# core_macos_build:
|
runs-on: macos-12
|
||||||
# runs-on: macos-11
|
|
||||||
|
|
||||||
# steps:
|
steps:
|
||||||
# - uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# - name: Run the test suite on macOS, build the demo project for iOS
|
- name: Run the test suite on macOS, build the demo project for iOS
|
||||||
# shell: bash
|
shell: bash
|
||||||
# run: |
|
run: |
|
||||||
# set -ex
|
set -ex
|
||||||
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
|
||||||
# # avoid building unrelated products for testing by specifying the test product explicitly
|
# avoid building unrelated products for testing by specifying the test product explicitly
|
||||||
# swift build --product TokamakPackageTests
|
swift build --product TokamakPackageTests
|
||||||
# `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||||
# (cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
||||||
|
|
||||||
# rm -rf Sources/TokamakGTKCHelpers/*.c
|
rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||||
|
|
||||||
# xcodebuild -version
|
xcodebuild -version
|
||||||
|
|
||||||
# # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||||
# # Disable macOS builds until Monterey is available on GHA.
|
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||||
# # xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||||
# # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
xcpretty --color
|
||||||
# # xcpretty --color
|
|
||||||
|
|
||||||
# cd "NativeDemo"
|
cd "NativeDemo"
|
||||||
# xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||||
# CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||||
# xcpretty --color
|
xcpretty --color
|
||||||
# cd ..
|
cd ..
|
||||||
|
|
||||||
# ./benchmark.sh
|
./benchmark.sh
|
||||||
|
|
||||||
# - name: Upload failed snapshots
|
- name: Upload failed snapshots
|
||||||
# uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
# if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
# with:
|
with:
|
||||||
# name: Failed snapshots
|
name: Failed snapshots
|
||||||
# path: '*Tests'
|
path: '*Tests'
|
||||||
|
|
||||||
|
# FIXME: disabled due to build errors, to be investigated
|
||||||
# gtk_macos_build:
|
# gtk_macos_build:
|
||||||
# runs-on: macos-11
|
# runs-on: macos-12
|
||||||
|
#
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v2
|
# - uses: actions/checkout@v2
|
||||||
# - name: Build the GTK renderer on macOS
|
# - name: Build the GTK renderer on macOS
|
||||||
# shell: bash
|
# shell: bash
|
||||||
# run: |
|
# run: |
|
||||||
# set -ex
|
# set -ex
|
||||||
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
|
||||||
|
#
|
||||||
# brew install gtk+3
|
# brew install gtk+3
|
||||||
|
#
|
||||||
# make build
|
# make build
|
||||||
|
|
||||||
gtk_ubuntu_18_04_build:
|
gtk_ubuntu_18_04_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-bionic
|
image: swiftlang/swift:nightly-5.7-bionic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -98,7 +110,7 @@ jobs:
|
||||||
gtk_ubuntu_20_04_build:
|
gtk_ubuntu_20_04_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-focal
|
image: swiftlang/swift:nightly-5.7-focal
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
|
@ -8,7 +8,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
codecov:
|
codecov:
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-focal
|
image: swiftlang/swift:nightly-5.7-focal
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||||
|
|
|
@ -24,6 +24,7 @@ jobs:
|
||||||
dependencies,
|
dependencies,
|
||||||
documentation,
|
documentation,
|
||||||
enhancement,
|
enhancement,
|
||||||
|
Fiber,
|
||||||
refactor,
|
refactor,
|
||||||
SwiftUI compatibility,
|
SwiftUI compatibility,
|
||||||
test suite,
|
test suite,
|
||||||
|
|
|
@ -8,5 +8,7 @@
|
||||||
--maxwidth 100
|
--maxwidth 100
|
||||||
--wraparguments before-first
|
--wraparguments before-first
|
||||||
--funcattributes prev-line
|
--funcattributes prev-line
|
||||||
|
--typeattributes prev-line
|
||||||
|
--varattributes prev-line
|
||||||
--disable andOperator
|
--disable andOperator
|
||||||
--swiftversion 5.4
|
--swiftversion 5.6
|
||||||
|
|
|
@ -29,6 +29,19 @@ only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conj
|
||||||
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
|
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
|
||||||
`TokamakAndroid` with no need to be aware of any of the web modules.
|
`TokamakAndroid` with no need to be aware of any of the web modules.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Tokamak uses [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library to
|
||||||
|
make sure that HTML and layout generated by renderers stay consistent. To run the test suite on macOS
|
||||||
|
you should use this command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, plain `swift test` won't work as it tries to build targets that aren't related to
|
||||||
|
the test suite.
|
||||||
|
|
||||||
### Coding Style
|
### Coding Style
|
||||||
|
|
||||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
||||||
|
|
|
@ -46,23 +46,23 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/swiftwasm/JavaScriptKit.git",
|
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
|
||||||
from: "0.15.0"
|
from: "0.15.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/OpenCombine/OpenCombine.git",
|
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
|
||||||
from: "0.12.0"
|
from: "0.12.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/swiftwasm/OpenCombineJS.git",
|
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
|
||||||
from: "0.2.0"
|
from: "0.2.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/google/swift-benchmark",
|
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
|
||||||
from: "0.1.2"
|
from: "0.1.2"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
|
||||||
from: "1.9.0"
|
from: "1.9.0"
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -135,6 +135,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||||
"TokamakCore",
|
"TokamakCore",
|
||||||
|
"TokamakTestRenderer",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
|
@ -194,6 +195,25 @@ let package = Package(
|
||||||
name: "TokamakTestRenderer",
|
name: "TokamakTestRenderer",
|
||||||
dependencies: ["TokamakCore"]
|
dependencies: ["TokamakCore"]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "TokamakLayoutTests",
|
||||||
|
dependencies: [
|
||||||
|
"TokamakCore",
|
||||||
|
"TokamakStaticHTML",
|
||||||
|
.product(
|
||||||
|
name: "SnapshotTesting",
|
||||||
|
package: "swift-snapshot-testing",
|
||||||
|
condition: .when(platforms: [.macOS])
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "TokamakReconcilerTests",
|
||||||
|
dependencies: [
|
||||||
|
"TokamakCore",
|
||||||
|
"TokamakTestRenderer",
|
||||||
|
]
|
||||||
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TokamakTests",
|
name: "TokamakTests",
|
||||||
dependencies: ["TokamakTestRenderer"]
|
dependencies: ["TokamakTestRenderer"]
|
||||||
|
|
37
README.md
37
README.md
|
@ -138,6 +138,31 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
|
||||||
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
||||||
app.
|
app.
|
||||||
|
|
||||||
|
### Fiber renderers
|
||||||
|
|
||||||
|
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
||||||
|
is optionally available. It can provide faster updates and allow for larger View hierarchies.
|
||||||
|
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
|
||||||
|
|
||||||
|
You can specify which reconciler to use in your `App`'s configuration:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct CounterApp: App {
|
||||||
|
static let _configuration: _AppConfiguration = .init(
|
||||||
|
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
|
||||||
|
reconciler: .fiber(useDynamicLayout: true)
|
||||||
|
)
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup("Counter Demo") {
|
||||||
|
Counter(count: 5, limit: 15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### For app developers
|
### For app developers
|
||||||
|
@ -147,7 +172,7 @@ app.
|
||||||
and macOS at the same time.
|
and macOS at the same time.
|
||||||
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
|
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
|
||||||
Other Linux distributions are currently not supported.
|
Other Linux distributions are currently not supported.
|
||||||
- [`carton` 0.14.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
||||||
|
|
||||||
### For users of apps depending on Tokamak
|
### For users of apps depending on Tokamak
|
||||||
|
|
||||||
|
@ -216,6 +241,9 @@ carton dev
|
||||||
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
||||||
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
||||||
|
|
||||||
|
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
|
||||||
|
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
||||||
|
@ -297,9 +325,10 @@ appreciated and helps in maintaining the project.
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
||||||
[David Hunt](https://github.com/foscomputerservices), [Ezra Berch](https://github.com/ezraberch),
|
[Ezra Berch](https://github.com/ezraberch),
|
||||||
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
|
[Jed Fox](https://jedfox.com),
|
||||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
|
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
|
||||||
|
[Yuta Saito](https://github.com/kateinoigakukun/).
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|
|
@ -40,14 +40,20 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct EmptyAnimatableData: VectorArithmetic {
|
@frozen
|
||||||
|
public struct EmptyAnimatableData: VectorArithmetic {
|
||||||
@inlinable
|
@inlinable
|
||||||
public init() {}
|
public init() {}
|
||||||
@inlinable public static var zero: Self { .init() }
|
|
||||||
|
@inlinable
|
||||||
|
public static var zero: Self { .init() }
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func += (lhs: inout Self, rhs: Self) {}
|
public static func += (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func -= (lhs: inout Self, rhs: Self) {}
|
public static func -= (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
.zero
|
.zero
|
||||||
|
@ -60,11 +66,15 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public mutating func scale(by rhs: Double) {}
|
public mutating func scale(by rhs: Double) {}
|
||||||
@inlinable public var magnitudeSquared: Double { .zero }
|
|
||||||
|
@inlinable
|
||||||
|
public var magnitudeSquared: Double { .zero }
|
||||||
|
|
||||||
public static func == (a: Self, b: Self) -> Bool { true }
|
public static func == (a: Self, b: Self) -> Bool { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct AnimatablePair<First, Second>: VectorArithmetic
|
@frozen
|
||||||
|
public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||||
where First: VectorArithmetic, Second: VectorArithmetic
|
where First: VectorArithmetic, Second: VectorArithmetic
|
||||||
{
|
{
|
||||||
public var first: First
|
public var first: First
|
||||||
|
@ -81,7 +91,8 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
set { (first, second) = newValue }
|
set { (first, second) = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@_transparent public static var zero: Self {
|
@_transparent
|
||||||
|
public static var zero: Self {
|
||||||
@_transparent get {
|
@_transparent get {
|
||||||
.init(First.zero, Second.zero)
|
.init(First.zero, Second.zero)
|
||||||
}
|
}
|
||||||
|
@ -115,7 +126,8 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
second.scale(by: rhs)
|
second.scale(by: rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@_transparent public var magnitudeSquared: Double {
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
@_transparent get {
|
@_transparent get {
|
||||||
first.magnitudeSquared + second.magnitudeSquared
|
first.magnitudeSquared + second.magnitudeSquared
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,8 @@ public struct _AnimationProxy {
|
||||||
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
|
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
|
where Value: Equatable
|
||||||
{
|
{
|
||||||
public var animation: Animation?
|
public var animation: Animation?
|
||||||
|
@ -155,7 +156,9 @@ public struct _AnimationProxy {
|
||||||
let content: Content
|
let content: Content
|
||||||
let animation: Animation?
|
let animation: Animation?
|
||||||
let value: Value
|
let value: Value
|
||||||
@State private var lastValue: Value?
|
|
||||||
|
@State
|
||||||
|
private var lastValue: Value?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content.transaction {
|
content.transaction {
|
||||||
|
@ -180,7 +183,8 @@ public struct _AnimationProxy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _AnimationView<Content>: View
|
@frozen
|
||||||
|
public struct _AnimationView<Content>: View
|
||||||
where Content: Equatable, Content: View
|
where Content: Equatable, Content: View
|
||||||
{
|
{
|
||||||
public var content: Content
|
public var content: Content
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
public struct Transaction {
|
public struct Transaction {
|
||||||
/// The overriden transaction for a state change in a `withTransaction` block.
|
/// The overridden transaction for a state change in a `withTransaction` block.
|
||||||
/// Is always set back to `nil` when the block exits.
|
/// Is always set back to `nil` when the block exits.
|
||||||
static var _active: Self?
|
static var _active: Self?
|
||||||
|
|
||||||
|
@ -50,7 +50,8 @@ protocol _TransactionModifierProtocol {
|
||||||
func modifyTransaction(_ transaction: inout Transaction)
|
func modifyTransaction(_ transaction: inout Transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _TransactionModifier: ViewModifier {
|
@frozen
|
||||||
|
public struct _TransactionModifier: ViewModifier {
|
||||||
public var transform: (inout Transaction) -> ()
|
public var transform: (inout Transaction) -> ()
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
|
@ -77,7 +78,8 @@ 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 content: V
|
||||||
public var base: _TransactionModifier
|
public var base: _TransactionModifier
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,9 @@ public protocol VectorArithmetic: AdditiveArithmetic {
|
||||||
extension Float: VectorArithmetic {
|
extension Float: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
|
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
|
||||||
@_transparent public var magnitudeSquared: Double {
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
@_transparent get { Double(self * self) }
|
@_transparent get { Double(self * self) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +35,9 @@ extension Float: VectorArithmetic {
|
||||||
extension Double: VectorArithmetic {
|
extension Double: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= rhs }
|
public mutating func scale(by rhs: Double) { self *= rhs }
|
||||||
@_transparent public var magnitudeSquared: Double {
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
@_transparent get { self * self }
|
@_transparent get { self * self }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +45,9 @@ extension Double: VectorArithmetic {
|
||||||
extension CGFloat: VectorArithmetic {
|
extension CGFloat: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
|
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
|
||||||
@_transparent public var magnitudeSquared: Double {
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
@_transparent get { Double(self * self) }
|
@_transparent get { Double(self * self) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import Foundation
|
||||||
public protocol _AnimationSolver {
|
public protocol _AnimationSolver {
|
||||||
/// Solve value at a specific point in time.
|
/// Solve value at a specific point in time.
|
||||||
func solve(at t: Double) -> Double
|
func solve(at t: Double) -> Double
|
||||||
/// Calculates the duration of the animation to a specific presision.
|
/// Calculates the duration of the animation to a specific precision.
|
||||||
func restingPoint(precision y: Double) -> Double
|
func restingPoint(precision y: Double) -> Double
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
// 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,7 +20,8 @@ import Foundation
|
||||||
public protocol _VectorMath: Animatable {}
|
public protocol _VectorMath: Animatable {}
|
||||||
|
|
||||||
public extension _VectorMath {
|
public extension _VectorMath {
|
||||||
@inlinable var magnitude: Double {
|
@inlinable
|
||||||
|
var magnitude: Double {
|
||||||
animatableData.magnitudeSquared.squareRoot()
|
animatableData.magnitudeSquared.squareRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,10 @@ public protocol App: _TitledApp {
|
||||||
var body: Body { get }
|
var body: Body { get }
|
||||||
|
|
||||||
/// Implemented by the renderer to mount the `App`
|
/// Implemented by the renderer to mount the `App`
|
||||||
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
|
static func _launch(
|
||||||
|
_ app: Self,
|
||||||
|
with configuration: _AppConfiguration
|
||||||
|
)
|
||||||
|
|
||||||
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
||||||
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
||||||
|
@ -36,14 +39,38 @@ public protocol App: _TitledApp {
|
||||||
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
||||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||||
|
|
||||||
|
static var _configuration: _AppConfiguration { get }
|
||||||
|
|
||||||
static func main()
|
static func main()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct _AppConfiguration {
|
||||||
|
public let reconciler: Reconciler
|
||||||
|
public let rootEnvironment: EnvironmentValues
|
||||||
|
|
||||||
|
public init(
|
||||||
|
reconciler: Reconciler = .stack,
|
||||||
|
rootEnvironment: EnvironmentValues = .init()
|
||||||
|
) {
|
||||||
|
self.reconciler = reconciler
|
||||||
|
self.rootEnvironment = rootEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Reconciler {
|
||||||
|
/// Use the `StackReconciler`.
|
||||||
|
case stack
|
||||||
|
/// Use the `FiberReconciler` with layout steps optionally enabled.
|
||||||
|
case fiber(useDynamicLayout: Bool = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public extension App {
|
public extension App {
|
||||||
|
static var _configuration: _AppConfiguration { .init() }
|
||||||
|
|
||||||
static func main() {
|
static func main() {
|
||||||
let app = Self()
|
let app = Self()
|
||||||
_launch(app, EnvironmentValues())
|
_launch(app, with: Self._configuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,13 @@
|
||||||
|
|
||||||
import OpenCombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
@propertyWrapper public struct AppStorage<Value>: DynamicProperty {
|
@propertyWrapper
|
||||||
|
public struct AppStorage<Value>: DynamicProperty {
|
||||||
let provider: _StorageProvider?
|
let provider: _StorageProvider?
|
||||||
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
|
|
||||||
|
@Environment(\._defaultAppStorage)
|
||||||
|
var defaultProvider: _StorageProvider?
|
||||||
|
|
||||||
var unwrappedProvider: _StorageProvider {
|
var unwrappedProvider: _StorageProvider {
|
||||||
provider ?? defaultProvider!
|
provider ?? defaultProvider!
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,23 @@ public protocol Scene {
|
||||||
// FIXME: If I put `@SceneBuilder` in front of this
|
// FIXME: If I put `@SceneBuilder` in front of this
|
||||||
// it fails to build with no useful error message.
|
// it fails to build with no useful error message.
|
||||||
var body: Self.Body { get }
|
var body: Self.Body { get }
|
||||||
|
|
||||||
|
/// Override the default implementation for `Scene`s with body types of `Never`
|
||||||
|
/// or in cases where the body would normally need to be type erased.
|
||||||
|
///
|
||||||
|
/// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor`
|
||||||
|
func _visitChildren<V: SceneVisitor>(_ visitor: V)
|
||||||
|
|
||||||
|
/// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom
|
||||||
|
/// `LayoutComputer` from the `SceneInputs`.
|
||||||
|
///
|
||||||
|
/// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`.
|
||||||
|
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public typealias SceneInputs<S: Scene> = ViewInputs<S>
|
||||||
|
public typealias SceneOutputs = ViewOutputs
|
||||||
|
|
||||||
protocol TitledScene {
|
protocol TitledScene {
|
||||||
var title: Text? { get }
|
var title: Text? { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,14 @@ public extension SceneBuilder {
|
||||||
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
||||||
C1: Scene
|
C1: Scene
|
||||||
{
|
{
|
||||||
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
|
_TupleScene(
|
||||||
|
(c0, c1),
|
||||||
|
children: [_AnyScene(c0), _AnyScene(c1)],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +44,15 @@ public extension SceneBuilder {
|
||||||
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
||||||
where C0: Scene, C1: Scene, C2: Scene
|
where C0: Scene, C1: Scene, C2: Scene
|
||||||
{
|
{
|
||||||
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
|
_TupleScene(
|
||||||
|
(c0, c1, c2),
|
||||||
|
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +65,13 @@ public extension SceneBuilder {
|
||||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||||
_TupleScene(
|
_TupleScene(
|
||||||
(c0, c1, c2, c3),
|
(c0, c1, c2, c3),
|
||||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)]
|
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +86,14 @@ public extension SceneBuilder {
|
||||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||||
_TupleScene(
|
_TupleScene(
|
||||||
(c0, c1, c2, c3, c4),
|
(c0, c1, c2, c3, c4),
|
||||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)]
|
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +118,15 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c3),
|
_AnyScene(c3),
|
||||||
_AnyScene(c4),
|
_AnyScene(c4),
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
]
|
],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
$0.visit(c5)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +153,16 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c4),
|
_AnyScene(c4),
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
]
|
],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
$0.visit(c5)
|
||||||
|
$0.visit(c6)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +191,17 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
]
|
],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
$0.visit(c5)
|
||||||
|
$0.visit(c6)
|
||||||
|
$0.visit(c7)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,7 +232,18 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
_AnyScene(c8),
|
_AnyScene(c8),
|
||||||
]
|
],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
$0.visit(c5)
|
||||||
|
$0.visit(c6)
|
||||||
|
$0.visit(c7)
|
||||||
|
$0.visit(c8)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +276,19 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
_AnyScene(c8),
|
_AnyScene(c8),
|
||||||
_AnyScene(c9),
|
_AnyScene(c9),
|
||||||
]
|
],
|
||||||
|
visit: {
|
||||||
|
$0.visit(c0)
|
||||||
|
$0.visit(c1)
|
||||||
|
$0.visit(c2)
|
||||||
|
$0.visit(c3)
|
||||||
|
$0.visit(c4)
|
||||||
|
$0.visit(c5)
|
||||||
|
$0.visit(c6)
|
||||||
|
$0.visit(c7)
|
||||||
|
$0.visit(c8)
|
||||||
|
$0.visit(c9)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,8 @@ public enum _DefaultSceneStorageProvider {
|
||||||
public static var `default`: _StorageProvider!
|
public static var `default`: _StorageProvider!
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
|
@propertyWrapper
|
||||||
|
public struct SceneStorage<Value>: DynamicProperty {
|
||||||
let key: String
|
let key: String
|
||||||
let defaultValue: Value
|
let defaultValue: Value
|
||||||
let store: (_StorageProvider, String, Value) -> ()
|
let store: (_StorageProvider, String, Value) -> ()
|
||||||
|
|
|
@ -75,4 +75,8 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
||||||
// public init(_ titleKey: LocalizedStringKey,
|
// public init(_ titleKey: LocalizedStringKey,
|
||||||
// @ViewBuilder content: () -> Content) {
|
// @ViewBuilder content: () -> Content) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
|
||||||
|
visitor.visit(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ public struct _AnyApp: App {
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
|
public static func _launch(_ app: Self, with configuration: _AppConfiguration) {
|
||||||
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
|
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,10 @@ public struct _AnyApp: App {
|
||||||
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var _configuration: _AppConfiguration {
|
||||||
|
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||||
|
}
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||||
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
||||||
|
|
|
@ -17,11 +17,17 @@
|
||||||
|
|
||||||
struct _TupleScene<T>: Scene, GroupScene {
|
struct _TupleScene<T>: Scene, GroupScene {
|
||||||
let value: T
|
let value: T
|
||||||
var children: [_AnyScene]
|
let children: [_AnyScene]
|
||||||
|
let visit: (SceneVisitor) -> ()
|
||||||
|
|
||||||
init(_ value: T, children: [_AnyScene]) {
|
init(
|
||||||
|
_ value: T,
|
||||||
|
children: [_AnyScene],
|
||||||
|
visit: @escaping (SceneVisitor) -> ()
|
||||||
|
) {
|
||||||
self.value = value
|
self.value = value
|
||||||
self.children = children
|
self.children = children
|
||||||
|
self.visit = visit
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: Never {
|
var body: Never {
|
||||||
|
|
|
@ -23,7 +23,8 @@ protocol EnvironmentReader {
|
||||||
mutating func setContent(from values: EnvironmentValues)
|
mutating func setContent(from values: EnvironmentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper public struct Environment<Value>: DynamicProperty {
|
@propertyWrapper
|
||||||
|
public struct Environment<Value>: DynamicProperty {
|
||||||
enum Content {
|
enum Content {
|
||||||
case keyPath(KeyPath<EnvironmentValues, Value>)
|
case keyPath(KeyPath<EnvironmentValues, Value>)
|
||||||
case value(Value)
|
case value(Value)
|
||||||
|
|
|
@ -17,11 +17,24 @@ public protocol EnvironmentKey {
|
||||||
static var defaultValue: Value { get }
|
static var defaultValue: Value { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol EnvironmentModifier {
|
/// 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 {
|
||||||
func modifyEnvironment(_ values: inout EnvironmentValues)
|
func modifyEnvironment(_ values: inout EnvironmentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
|
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 let keyPath: WritableKeyPath<EnvironmentValues, Value>
|
public let keyPath: WritableKeyPath<EnvironmentValues, Value>
|
||||||
public let value: Value
|
public let value: Value
|
||||||
|
|
||||||
|
@ -32,7 +45,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo
|
||||||
|
|
||||||
public typealias Body = Never
|
public typealias Body = Never
|
||||||
|
|
||||||
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
public func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||||
values[keyPath: keyPath] = value
|
values[keyPath: keyPath] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,12 @@
|
||||||
|
|
||||||
import OpenCombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
|
@propertyWrapper
|
||||||
|
public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||||
where ObjectType: ObservableObject
|
where ObjectType: ObservableObject
|
||||||
{
|
{
|
||||||
@dynamicMemberLookup public struct Wrapper {
|
@dynamicMemberLookup
|
||||||
|
public struct Wrapper {
|
||||||
internal let root: ObjectType
|
internal let root: ObjectType
|
||||||
public subscript<Subject>(
|
public subscript<Subject>(
|
||||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||||
|
|
|
@ -76,7 +76,7 @@ public extension EnvironmentValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
|
||||||
let environmentValues: EnvironmentValues
|
let environmentValues: EnvironmentValues
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
// 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)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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)
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// 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) ?? "")
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,626 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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)
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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 }
|
||||||
|
}
|
|
@ -0,0 +1,477 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,268 @@
|
||||||
|
// 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) }
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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)
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// 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() }
|
||||||
|
}
|
|
@ -0,0 +1,296 @@
|
||||||
|
// 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() }
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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>
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// 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)]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// 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>
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
// 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,7 +14,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public enum ContentMode: Hashable, CaseIterable {
|
@frozen
|
||||||
|
public enum ContentMode: Hashable, CaseIterable {
|
||||||
case fit
|
case fit
|
||||||
case fill
|
case fill
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct _OffsetEffect: GeometryEffect, Equatable {
|
@frozen
|
||||||
|
public struct _OffsetEffect: GeometryEffect, Equatable {
|
||||||
public var offset: CGSize
|
public var offset: CGSize
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct _ScaleEffect: GeometryEffect, Equatable {
|
@frozen
|
||||||
|
public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||||
public var scale: CGSize
|
public var scale: CGSize
|
||||||
public var anchor: UnitPoint
|
public var anchor: UnitPoint
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,16 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
protocol ModifierContainer {
|
protocol ModifierContainer {
|
||||||
var environmentModifier: EnvironmentModifier? { get }
|
var environmentModifier: _EnvironmentModifier? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ModifiedContentProtocol {}
|
protocol ModifiedContentProtocol {}
|
||||||
|
|
||||||
/// A value with a modifier applied to it.
|
/// A value with a modifier applied to it.
|
||||||
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||||
@Environment(\.self) public var environment
|
@Environment(\.self)
|
||||||
|
public var environment
|
||||||
|
|
||||||
public typealias Body = Never
|
public typealias Body = Never
|
||||||
public private(set) var content: Content
|
public private(set) var content: Content
|
||||||
public private(set) var modifier: Modifier
|
public private(set) var modifier: Modifier
|
||||||
|
@ -32,7 +34,7 @@ public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: ModifierContainer {
|
extension ModifiedContent: ModifierContainer {
|
||||||
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
|
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||||
|
|
|
@ -24,6 +24,18 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
||||||
public let content: Content
|
public let content: Content
|
||||||
public let background: Background
|
public let background: Background
|
||||||
public let alignment: Alignment
|
public let alignment: Alignment
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public init(content: Content, background: Background, alignment: Alignment) {
|
||||||
|
self.content = content
|
||||||
|
self.background = background
|
||||||
|
self.alignment = alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitor.visit(background)
|
||||||
|
visitor.visit(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||||
|
@ -77,7 +89,8 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
@frozen
|
||||||
|
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||||
where Style: ShapeStyle, Bounds: Shape
|
where Style: ShapeStyle, Bounds: Shape
|
||||||
{
|
{
|
||||||
public var environment: EnvironmentValues!
|
public var environment: EnvironmentValues!
|
||||||
|
@ -129,6 +142,11 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
||||||
public let content: Content
|
public let content: Content
|
||||||
public let overlay: Overlay
|
public let overlay: Overlay
|
||||||
public let alignment: Alignment
|
public let alignment: Alignment
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitor.visit(content)
|
||||||
|
visitor.visit(overlay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||||
|
|
|
@ -16,6 +16,23 @@ public protocol ViewModifier {
|
||||||
typealias Content = _ViewModifier_Content<Self>
|
typealias Content = _ViewModifier_Content<Self>
|
||||||
associatedtype Body: View
|
associatedtype Body: View
|
||||||
func body(content: Content) -> Self.Body
|
func body(content: Content) -> Self.Body
|
||||||
|
|
||||||
|
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
|
||||||
|
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ViewModifier {
|
||||||
|
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(inputs: inputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||||
|
if Body.self == Never.self {
|
||||||
|
content.visitChildren(visitor)
|
||||||
|
} else {
|
||||||
|
visitor.visit(body(content: content))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _ViewModifier_Content<Modifier>: View
|
public struct _ViewModifier_Content<Modifier>: View
|
||||||
|
@ -23,15 +40,27 @@ public struct _ViewModifier_Content<Modifier>: View
|
||||||
{
|
{
|
||||||
public let modifier: Modifier
|
public let modifier: Modifier
|
||||||
public let view: AnyView
|
public let view: AnyView
|
||||||
|
let visitChildren: (ViewVisitor) -> ()
|
||||||
|
|
||||||
public init(modifier: Modifier, view: AnyView) {
|
public init(modifier: Modifier, view: AnyView) {
|
||||||
self.modifier = modifier
|
self.modifier = modifier
|
||||||
self.view = view
|
self.view = view
|
||||||
|
visitChildren = { $0.visit(view) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init<V: View>(modifier: Modifier, view: V) {
|
||||||
|
self.modifier = modifier
|
||||||
|
self.view = AnyView(view)
|
||||||
|
visitChildren = { $0.visit(view) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitChildren(visitor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -78,9 +78,6 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
|
|
||||||
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
||||||
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
||||||
if let parent = parent {
|
|
||||||
parent.preferenceStore.merge(with: self.preferenceStore)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
||||||
|
|
|
@ -94,13 +94,9 @@ public class MountedElement<R: Renderer> {
|
||||||
|
|
||||||
public internal(set) var environmentValues: EnvironmentValues
|
public internal(set) var environmentValues: EnvironmentValues
|
||||||
|
|
||||||
weak var parent: MountedElement<R>?
|
private(set) weak var parent: MountedElement<R>?
|
||||||
/// `didSet` on this field propagates the preference changes up the view tree.
|
|
||||||
var preferenceStore: _PreferenceStore = .init() {
|
var preferenceStore: _PreferenceStore = .init()
|
||||||
didSet {
|
|
||||||
parent?.preferenceStore.merge(with: preferenceStore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public internal(set) var viewTraits: _ViewTraitStore
|
public internal(set) var viewTraits: _ViewTraitStore
|
||||||
|
|
||||||
|
@ -110,6 +106,7 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
viewTraits = .init()
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
|
@ -118,6 +115,7 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
viewTraits = .init()
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
@ -131,6 +129,7 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
self.viewTraits = viewTraits
|
self.viewTraits = viewTraits
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateEnvironment() {
|
func updateEnvironment() {
|
||||||
|
@ -145,6 +144,10 @@ public class MountedElement<R: Renderer> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func connectParentPreferenceStore() {
|
||||||
|
preferenceStore.parent = parent?.preferenceStore
|
||||||
|
}
|
||||||
|
|
||||||
/// You must call `super.prepareForMount` before all other mounting work.
|
/// You must call `super.prepareForMount` before all other mounting work.
|
||||||
func prepareForMount(with transaction: Transaction) {
|
func prepareForMount(with transaction: Transaction) {
|
||||||
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||||
|
|
|
@ -25,12 +25,41 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
|
||||||
static var defaultValue: Value { nil }
|
static var defaultValue: Value { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _PreferenceValue<Key> where Key: PreferenceKey {
|
final class _PreferenceValueStorage: CustomDebugStringConvertible {
|
||||||
/// Every value the `Key` has had.
|
/// Every value the `Key` has had.
|
||||||
var valueList: [Key.Value]
|
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
|
||||||
|
}
|
||||||
|
|
||||||
/// The latest value.
|
/// The latest value.
|
||||||
public var value: Key.Value {
|
public var value: Key.Value {
|
||||||
reduce(valueList)
|
reduce(storage.valueList.compactMap { $0 as? Key.Value })
|
||||||
}
|
}
|
||||||
|
|
||||||
func reduce(_ values: [Key.Value]) -> Key.Value {
|
func reduce(_ values: [Key.Value]) -> Key.Value {
|
||||||
|
@ -48,38 +77,79 @@ public extension _PreferenceValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _PreferenceStore {
|
public final class _PreferenceStore: CustomDebugStringConvertible {
|
||||||
|
/// The values of the `_PreferenceStore` on the last update.
|
||||||
|
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
|
||||||
/// The backing values of the `_PreferenceStore`.
|
/// The backing values of the `_PreferenceStore`.
|
||||||
private var values: [String: Any]
|
private var values: [ObjectIdentifier: _PreferenceValueStorage]
|
||||||
|
|
||||||
public init(values: [String: Any] = [:]) {
|
weak var parent: _PreferenceStore?
|
||||||
|
|
||||||
|
public var debugDescription: String {
|
||||||
|
"Preferences (\(ObjectIdentifier(self))): \(values)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
|
||||||
|
previousValues = [:]
|
||||||
self.values = values
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve a late-binding token for `key`, or save the default value if it does not yet exist.
|
||||||
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
values[String(reflecting: key)] as? _PreferenceValue<Key>
|
let keyID = ObjectIdentifier(key)
|
||||||
?? _PreferenceValue(valueList: [Key.defaultValue])
|
let storage: _PreferenceValueStorage
|
||||||
|
if let existing = values[keyID] {
|
||||||
|
storage = existing
|
||||||
|
} else {
|
||||||
|
storage = .init(key)
|
||||||
|
values[keyID] = storage
|
||||||
|
}
|
||||||
|
return _PreferenceValue(storage: storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
/// 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>
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
let previousValues = self.value(forKey: key).valueList
|
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key))
|
||||||
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public mutating func merge(with other: Self) {
|
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||||
self = merging(with: other)
|
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 func merging(with other: Self) -> Self {
|
func merge(_ other: _PreferenceStore) {
|
||||||
var result = values
|
for (key, otherStorage) in other.values {
|
||||||
for (key, value) in other.values {
|
if let storage = values[key] {
|
||||||
result[key] = value
|
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()
|
||||||
}
|
}
|
||||||
return .init(values: result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,12 +25,26 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
|
||||||
|
|
||||||
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
||||||
let value = preferenceStore.value(forKey: Key.self)
|
let value = preferenceStore.value(forKey: Key.self)
|
||||||
let previousValue = value.reduce(value.valueList.dropLast())
|
let previousValue = value.reduce((value.storage.valueList as? [Key.Value] ?? []).dropLast())
|
||||||
if previousValue != value.value {
|
if previousValue != value.value {
|
||||||
action(value.value)
|
action(value.value)
|
||||||
}
|
}
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: {
|
||||||
|
let value = $0.value(forKey: Key.self).value
|
||||||
|
let previousValue = $0.previousValue(forKey: Key.self).value
|
||||||
|
if value != previousValue {
|
||||||
|
inputs.content.action(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -20,11 +20,12 @@
|
||||||
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
|
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
|
||||||
where Key: PreferenceKey, Content: View
|
where Key: PreferenceKey, Content: View
|
||||||
{
|
{
|
||||||
@State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
|
@State
|
||||||
valueList: [Key.defaultValue]
|
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self))
|
||||||
)
|
|
||||||
public let transform: (_PreferenceValue<Key>) -> Content
|
public let transform: (_PreferenceValue<Key>) -> Content
|
||||||
|
|
||||||
|
private var valueReference: _PreferenceValue<Key>?
|
||||||
|
|
||||||
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
||||||
self.transform = transform
|
self.transform = transform
|
||||||
}
|
}
|
||||||
|
@ -34,7 +35,18 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
transform(resolvedValue)
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +81,7 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.overlay(transform($0.value)) }
|
Key._delay { self.overlay($0._force(transform)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func backgroundPreferenceValue<Key, T>(
|
func backgroundPreferenceValue<Key, T>(
|
||||||
|
@ -78,6 +90,6 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.background(transform($0.value)) }
|
Key._delay { self.background($0._force(transform)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,18 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
|
||||||
preferenceStore.insert(newValue, forKey: Key.self)
|
preferenceStore.insert(newValue, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: {
|
||||||
|
var value = $0.value(forKey: Key.self).value
|
||||||
|
inputs.content.transform(&value)
|
||||||
|
$0.insert(value, forKey: Key.self)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -27,6 +27,14 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
|
||||||
preferenceStore.insert(value, forKey: Key.self)
|
preferenceStore.insert(value, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: { $0.insert(inputs.content.value, forKey: Key.self) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
||||||
|
|
|
@ -18,7 +18,8 @@ import Foundation
|
||||||
public protocol PreviewProvider {
|
public protocol PreviewProvider {
|
||||||
associatedtype Previews: View
|
associatedtype Previews: View
|
||||||
|
|
||||||
@ViewBuilder static var previews: Previews { get }
|
@ViewBuilder
|
||||||
|
static var previews: Previews { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {
|
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {
|
||||||
|
|
|
@ -20,7 +20,8 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
typealias FieldTypeAccessor = @convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
|
typealias FieldTypeAccessor = @convention(c)
|
||||||
|
(UnsafePointer<Int>) -> UnsafePointer<Int>
|
||||||
|
|
||||||
struct StructTypeDescriptor {
|
struct StructTypeDescriptor {
|
||||||
let flags: Int32
|
let flags: Int32
|
||||||
|
|
|
@ -20,7 +20,21 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
public struct PropertyInfo {
|
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 let name: String
|
public let name: String
|
||||||
public let type: Any.Type
|
public let type: Any.Type
|
||||||
public let isVar: Bool
|
public let isVar: Bool
|
||||||
|
|
|
@ -24,7 +24,9 @@ struct MetadataOffset<Pointee> {
|
||||||
let offset: Int32
|
let offset: Int32
|
||||||
|
|
||||||
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
|
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
|
||||||
#if arch(wasm32)
|
// 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)
|
||||||
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
|
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
|
||||||
#else
|
#else
|
||||||
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)
|
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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,7 +38,8 @@ extension ContainerRelativeShape: InsettableShape {
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
@frozen internal struct _Inset: InsettableShape, DynamicProperty {
|
@frozen
|
||||||
|
internal struct _Inset: InsettableShape, DynamicProperty {
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
internal var amount: CGFloat
|
internal var amount: CGFloat
|
||||||
@inlinable
|
@inlinable
|
||||||
|
@ -76,7 +77,8 @@ 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
|
public var shape: Shape
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(shape: Shape) { self.shape = shape }
|
public init(shape: Shape) { self.shape = shape }
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
|
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 shape: S
|
||||||
public var style: StrokeStyle
|
public var style: StrokeStyle
|
||||||
|
|
||||||
|
|
|
@ -89,8 +89,11 @@ extension RoundedRectangle: InsettableShape {
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct _Inset: InsettableShape {
|
struct _Inset: InsettableShape {
|
||||||
@usableFromInline var base: RoundedRectangle
|
@usableFromInline
|
||||||
@usableFromInline var amount: CGFloat
|
var base: RoundedRectangle
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
var amount: CGFloat
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
init(base: RoundedRectangle, amount: CGFloat) {
|
init(base: RoundedRectangle, amount: CGFloat) {
|
||||||
|
|
|
@ -21,6 +21,18 @@ public protocol Shape: Animatable, View {
|
||||||
func path(in rect: CGRect) -> Path
|
func path(in rect: CGRect) -> Path
|
||||||
|
|
||||||
static var role: ShapeRole { get }
|
static var role: ShapeRole { get }
|
||||||
|
|
||||||
|
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Shape {
|
||||||
|
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
||||||
|
// TODO: Check if SwiftUI changes this behavior.
|
||||||
|
|
||||||
|
// SwiftUI seems to not compute the path at all and just return
|
||||||
|
// the following.
|
||||||
|
proposal.replacingUnspecifiedDimensions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ShapeRole: Hashable {
|
public enum ShapeRole: Hashable {
|
||||||
|
@ -54,9 +66,15 @@ public struct FillStyle: Equatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
|
public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content: Shape,
|
||||||
@Environment(\.self) public var environment
|
Style: ShapeStyle
|
||||||
@Environment(\.foregroundColor) public var foregroundColor
|
{
|
||||||
|
@Environment(\.self)
|
||||||
|
public var environment
|
||||||
|
|
||||||
|
@Environment(\.foregroundColor)
|
||||||
|
public var foregroundColor
|
||||||
|
|
||||||
public var shape: Content
|
public var shape: Content
|
||||||
public var style: Style
|
public var style: Style
|
||||||
public var fillStyle: FillStyle
|
public var fillStyle: FillStyle
|
||||||
|
@ -66,6 +84,32 @@ public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, S
|
||||||
self.style = style
|
self.style = style
|
||||||
self.fillStyle = fillStyle
|
self.fillStyle = fillStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
|
||||||
|
.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sizeThatFits(
|
||||||
|
proposal: ProposedViewSize,
|
||||||
|
subviews: Subviews,
|
||||||
|
cache: inout ()
|
||||||
|
) -> CGSize {
|
||||||
|
proposal.replacingUnspecifiedDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func placeSubviews(
|
||||||
|
in bounds: CGRect,
|
||||||
|
proposal: ProposedViewSize,
|
||||||
|
subviews: Subviews,
|
||||||
|
cache: inout ()
|
||||||
|
) {
|
||||||
|
for subview in subviews {
|
||||||
|
subview.place(
|
||||||
|
at: bounds.origin,
|
||||||
|
proposal: .init(width: bounds.width, height: bounds.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Shape {
|
public extension Shape {
|
||||||
|
|
|
@ -56,7 +56,8 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _BackgroundStyleModifier<Style>: ViewModifier, EnvironmentModifier,
|
@frozen
|
||||||
|
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
|
||||||
EnvironmentReader
|
EnvironmentReader
|
||||||
where Style: ShapeStyle
|
where Style: ShapeStyle
|
||||||
{
|
{
|
||||||
|
|
|
@ -71,9 +71,10 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _ForegroundStyleModifier<
|
@frozen
|
||||||
|
public struct _ForegroundStyleModifier<
|
||||||
Primary, Secondary, Tertiary
|
Primary, Secondary, Tertiary
|
||||||
>: ViewModifier, EnvironmentModifier
|
>: ViewModifier, _EnvironmentModifier
|
||||||
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
|
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
|
||||||
{
|
{
|
||||||
public var primary: Primary
|
public var primary: Primary
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct AngularGradient: ShapeStyle, View {
|
@frozen
|
||||||
|
public struct AngularGradient: ShapeStyle, View {
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startAngle: Angle
|
internal var startAngle: Angle
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct EllipticalGradient: ShapeStyle, View {
|
@frozen
|
||||||
|
public struct EllipticalGradient: ShapeStyle, View {
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startRadiusFraction: CGFloat
|
internal var startRadiusFraction: CGFloat
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct Gradient: Equatable {
|
@frozen
|
||||||
@frozen public struct Stop: Equatable {
|
public struct Gradient: Equatable {
|
||||||
|
@frozen
|
||||||
|
public struct Stop: Equatable {
|
||||||
public var color: Color
|
public var color: Color
|
||||||
public var location: CGFloat
|
public var location: CGFloat
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct LinearGradient: ShapeStyle, View {
|
@frozen
|
||||||
|
public struct LinearGradient: ShapeStyle, View {
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var startPoint: UnitPoint
|
internal var startPoint: UnitPoint
|
||||||
internal var endPoint: UnitPoint
|
internal var endPoint: UnitPoint
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct RadialGradient: ShapeStyle, View {
|
@frozen
|
||||||
|
public struct RadialGradient: ShapeStyle, View {
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startRadius: CGFloat
|
internal var startRadius: CGFloat
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
|
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
|
||||||
@frozen public struct HierarchicalShapeStyle: ShapeStyle {
|
@frozen
|
||||||
|
public struct HierarchicalShapeStyle: ShapeStyle {
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
internal var id: UInt32
|
internal var id: UInt32
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
#if canImport(CoreGraphics)
|
||||||
|
import CoreGraphics
|
||||||
|
#endif
|
||||||
|
|
||||||
public struct StrokeStyle: Equatable {
|
public struct StrokeStyle: Equatable {
|
||||||
public var lineWidth: CGFloat
|
public var lineWidth: CGFloat
|
||||||
public var lineCap: CGLineCap
|
public var lineCap: CGLineCap
|
||||||
|
|
|
@ -58,6 +58,12 @@ public final class StackReconciler<R: Renderer> {
|
||||||
*/
|
*/
|
||||||
public let rootTarget: R.TargetType
|
public let rootTarget: R.TargetType
|
||||||
|
|
||||||
|
/** A root renderer's main preference store.
|
||||||
|
*/
|
||||||
|
public var preferenceStore: _PreferenceStore {
|
||||||
|
rootElement.preferenceStore
|
||||||
|
}
|
||||||
|
|
||||||
/** A root of the mounted elements tree to which all other mounted elements are attached to.
|
/** A root of the mounted elements tree to which all other mounted elements are attached to.
|
||||||
*/
|
*/
|
||||||
private let rootElement: MountedElement<R>
|
private let rootElement: MountedElement<R>
|
||||||
|
|
|
@ -23,7 +23,8 @@ protocol WritableValueStorage: ValueStorage {
|
||||||
var setter: ((Any, Transaction) -> ())? { get set }
|
var setter: ((Any, Transaction) -> ())? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper public struct State<Value>: DynamicProperty {
|
@propertyWrapper
|
||||||
|
public struct State<Value>: DynamicProperty {
|
||||||
private let initialValue: Value
|
private let initialValue: Value
|
||||||
|
|
||||||
var anyInitialValue: Any { initialValue }
|
var anyInitialValue: Any { initialValue }
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
|
|
||||||
public struct ToggleStyleConfiguration {
|
public struct ToggleStyleConfiguration {
|
||||||
public let label: AnyView
|
public let label: AnyView
|
||||||
@Binding public var isOn: Swift.Bool
|
@Binding
|
||||||
|
public var isOn: Swift.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ToggleStyle {
|
public protocol ToggleStyle {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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,11 +37,13 @@ struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
|
||||||
case tagged(V)
|
case tagged(V)
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable static var defaultValue: Value { .untagged }
|
@inlinable
|
||||||
|
static var defaultValue: Value { .untagged }
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
|
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
|
||||||
@inlinable static var defaultValue: Bool { false }
|
@inlinable
|
||||||
|
static var defaultValue: Bool { false }
|
||||||
@usableFromInline typealias Value = Bool
|
@usableFromInline typealias Value = Bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen public struct AnyTransition {
|
@frozen
|
||||||
|
public struct AnyTransition {
|
||||||
fileprivate let box: _AnyTransitionBox
|
fileprivate let box: _AnyTransitionBox
|
||||||
|
|
||||||
private init(_ box: _AnyTransitionBox) {
|
private init(_ box: _AnyTransitionBox) {
|
||||||
|
@ -27,14 +28,16 @@ import Foundation
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct TransitionTraitKey: _ViewTraitKey {
|
struct TransitionTraitKey: _ViewTraitKey {
|
||||||
@inlinable static var defaultValue: AnyTransition { .opacity }
|
@inlinable
|
||||||
|
static var defaultValue: AnyTransition { .opacity }
|
||||||
|
|
||||||
@usableFromInline typealias Value = AnyTransition
|
@usableFromInline typealias Value = AnyTransition
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct CanTransitionTraitKey: _ViewTraitKey {
|
struct CanTransitionTraitKey: _ViewTraitKey {
|
||||||
@inlinable static var defaultValue: Bool { false }
|
@inlinable
|
||||||
|
static var defaultValue: Bool { false }
|
||||||
|
|
||||||
@usableFromInline typealias Value = Bool
|
@usableFromInline typealias Value = Bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,8 @@ public protocol _TraitWritingModifierProtocol {
|
||||||
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
@frozen
|
||||||
|
public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
||||||
where Trait: _ViewTraitKey
|
where Trait: _ViewTraitKey
|
||||||
{
|
{
|
||||||
public let value: Trait.Value
|
public let value: Trait.Value
|
||||||
|
@ -40,6 +41,14 @@ public protocol _TraitWritingModifierProtocol {
|
||||||
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
||||||
viewTraitStore.insert(value, forKey: Trait.self)
|
viewTraitStore.insert(value, forKey: Trait.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<_TraitWritingModifier<Trait>>)
|
||||||
|
-> ViewOutputs
|
||||||
|
{
|
||||||
|
var store = inputs.traits ?? .init()
|
||||||
|
store.insert(inputs.content.value, forKey: Trait.self)
|
||||||
|
return .init(inputs: inputs, traits: store)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: _TraitWritingModifierProtocol
|
extension ModifiedContent: _TraitWritingModifierProtocol
|
||||||
|
|
|
@ -16,21 +16,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
public struct _ViewTraitStore {
|
public struct _ViewTraitStore {
|
||||||
public var values = [String: Any]()
|
public var values = [ObjectIdentifier: Any]()
|
||||||
|
|
||||||
public init(values: [String: Any] = [:]) {
|
public init(values: [ObjectIdentifier: Any] = [:]) {
|
||||||
self.values = values
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
||||||
where Key: _ViewTraitKey
|
where Key: _ViewTraitKey
|
||||||
{
|
{
|
||||||
values[String(reflecting: key)] as? Key.Value ?? Key.defaultValue
|
values[ObjectIdentifier(key)] as? Key.Value ?? Key.defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||||
where Key: _ViewTraitKey
|
where Key: _ViewTraitKey
|
||||||
{
|
{
|
||||||
values[String(reflecting: key)] = value
|
values[ObjectIdentifier(key)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ public struct AnyView: _PrimitiveView {
|
||||||
*/
|
*/
|
||||||
let bodyType: Any.Type
|
let bodyType: Any.Type
|
||||||
|
|
||||||
|
let visitChildren: (ViewVisitor, Any) -> ()
|
||||||
|
|
||||||
public init<V>(_ view: V) where V: View {
|
public init<V>(_ view: V) where V: View {
|
||||||
if let anyView = view as? AnyView {
|
if let anyView = view as? AnyView {
|
||||||
self = anyView
|
self = anyView
|
||||||
|
@ -52,8 +54,14 @@ public struct AnyView: _PrimitiveView {
|
||||||
self.view = view
|
self.view = view
|
||||||
// swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
bodyClosure = { AnyView(($0 as! V).body) }
|
bodyClosure = { AnyView(($0 as! V).body) }
|
||||||
|
// swiftlint:disable:next force_cast
|
||||||
|
visitChildren = { $0.visit($1 as! V) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitChildren(visitor, view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
||||||
|
|
|
@ -24,7 +24,9 @@ public struct Canvas<Symbols> where Symbols: View {
|
||||||
public var colorMode: ColorRenderingMode
|
public var colorMode: ColorRenderingMode
|
||||||
public var rendersAsynchronously: Bool
|
public var rendersAsynchronously: Bool
|
||||||
|
|
||||||
@Environment(\.self) public var _environment: EnvironmentValues
|
@Environment(\.self)
|
||||||
|
public var _environment: EnvironmentValues
|
||||||
|
|
||||||
public func _makeContext(
|
public func _makeContext(
|
||||||
onOperation: @escaping (GraphicsContext._Storage, GraphicsContext._Storage._Operation) -> (),
|
onOperation: @escaping (GraphicsContext._Storage, GraphicsContext._Storage._Operation) -> (),
|
||||||
imageResolver: @escaping (Image, EnvironmentValues) -> GraphicsContext.ResolvedImage,
|
imageResolver: @escaping (Image, EnvironmentValues) -> GraphicsContext.ResolvedImage,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue