Compare commits
103 Commits
100-percen
...
main
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 | |
![]() |
5b7e7058e8 | |
![]() |
337b80a4c4 | |
![]() |
528fe056e0 | |
![]() |
39d37a96c8 | |
![]() |
6905fdff19 | |
![]() |
fd64eafde8 | |
![]() |
eef6bb2da3 | |
![]() |
1952170ce8 | |
![]() |
3c649d5ead | |
![]() |
12606a809e | |
![]() |
a9addc8cb1 | |
![]() |
077c0cdfcb | |
![]() |
5c3bc9d783 | |
![]() |
d2d79c2bbf | |
![]() |
32616fe9d4 | |
![]() |
cbfdc34793 | |
![]() |
a5a05b4826 | |
![]() |
2ad85b329d | |
![]() |
f07b9fa883 | |
![]() |
dfc79e4148 | |
![]() |
005996262a | |
![]() |
da66063918 | |
![]() |
8788fd64e9 | |
![]() |
88cee68bee | |
![]() |
19bcf2746b | |
![]() |
3d9558f1b8 | |
![]() |
e1fcd180d7 | |
![]() |
bd1d8138c3 | |
![]() |
c2ed28ca40 | |
![]() |
4609b0a203 | |
![]() |
21c21cd328 | |
![]() |
a8c6eae94e | |
![]() |
9a568ab9cf | |
![]() |
ae0db4d1f1 | |
![]() |
22ea230ce0 | |
![]() |
6792dbb02c | |
![]() |
12a6256ec0 | |
![]() |
ab5e564ada | |
![]() |
a064956095 | |
![]() |
ff3f81dbfd | |
![]() |
b6790c5c6d | |
![]() |
30f55d9814 | |
![]() |
2efa80a57d | |
![]() |
4a7748ad6b | |
![]() |
54146b8a38 | |
![]() |
79a9a66da2 | |
![]() |
738455be68 | |
![]() |
719c109811 | |
![]() |
2dcbc67cd3 | |
![]() |
9aa88a1978 | |
![]() |
d35e37c4f5 | |
![]() |
e6c37a4c80 | |
![]() |
ae219e947b | |
![]() |
ac69bbc3e5 | |
![]() |
3302a5163c | |
![]() |
5926e9f182 | |
![]() |
da9843d07f | |
![]() |
44280847cf | |
![]() |
77759777f1 | |
![]() |
096ec5c4a2 | |
![]() |
57f57174c5 | |
![]() |
4a372c3fa8 | |
![]() |
d914e1bdc9 | |
![]() |
5c458f92b8 | |
![]() |
bde7de9be0 | |
![]() |
8076035120 | |
![]() |
4d211af563 | |
![]() |
4a1101c21a | |
![]() |
19cf8b6782 | |
![]() |
eeddfe9e4b |
|
@ -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. iPhone6]
|
- 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.
|
||||||
|
|
|
@ -6,17 +6,39 @@ on:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
swiftwasm_build:
|
swiftwasm_bundle_5_6:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: swiftwasm/swiftwasm-action@v5.3
|
- 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:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { toolchain: wasm-5.6.0-RELEASE }
|
||||||
|
- { toolchain: wasm-5.7-SNAPSHOT-2022-07-27-a }
|
||||||
|
- { toolchain: wasm-DEVELOPMENT-SNAPSHOT-2022-07-23-a }
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: echo "${{ matrix.toolchain }}" > .swift-version
|
||||||
|
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||||
|
with:
|
||||||
|
shell-action: carton test --environment node
|
||||||
|
|
||||||
core_macos_build:
|
core_macos_build:
|
||||||
runs-on: macos-11.0
|
runs-on: macos-12
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -24,16 +46,17 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -ex
|
set -ex
|
||||||
sudo xcode-select --switch /Applications/Xcode_12.3.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)
|
||||||
|
|
||||||
rm -rf Sources/TokamakGTKCHelpers/*.c
|
rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||||
|
|
||||||
xcodebuild -version
|
xcodebuild -version
|
||||||
|
|
||||||
# make sure Tokamak can be built on macOS so that Xcode autocomplete works
|
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||||
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
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
|
||||||
|
@ -46,23 +69,33 @@ jobs:
|
||||||
|
|
||||||
./benchmark.sh
|
./benchmark.sh
|
||||||
|
|
||||||
gtk_macos_build:
|
- name: Upload failed snapshots
|
||||||
runs-on: macos-latest
|
uses: actions/upload-artifact@v2
|
||||||
|
if: ${{ failure() }}
|
||||||
|
with:
|
||||||
|
name: Failed snapshots
|
||||||
|
path: '*Tests'
|
||||||
|
|
||||||
steps:
|
# FIXME: disabled due to build errors, to be investigated
|
||||||
- uses: actions/checkout@v2
|
# gtk_macos_build:
|
||||||
- name: Build the GTK renderer on macOS
|
# runs-on: macos-12
|
||||||
shell: bash
|
#
|
||||||
run: |
|
# steps:
|
||||||
set -ex
|
# - uses: actions/checkout@v2
|
||||||
sudo xcode-select --switch /Applications/Xcode_12.3.app/Contents/Developer/
|
# - name: Build the GTK renderer on macOS
|
||||||
|
# shell: bash
|
||||||
brew install gtk+3
|
# run: |
|
||||||
|
# set -ex
|
||||||
make build
|
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
|
||||||
|
#
|
||||||
|
# brew install gtk+3
|
||||||
|
#
|
||||||
|
# make build
|
||||||
|
|
||||||
gtk_ubuntu_18_04_build:
|
gtk_ubuntu_18_04_build:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: swiftlang/swift:nightly-5.7-bionic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -70,13 +103,14 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -ex
|
set -ex
|
||||||
sudo apt-get update
|
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
|
||||||
sudo apt-get install libgtk+-3.0 gtk+-3.0
|
|
||||||
|
|
||||||
make build
|
make build
|
||||||
|
|
||||||
gtk_ubuntu_20_04_build:
|
gtk_ubuntu_20_04_build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: swiftlang/swift:nightly-5.7-focal
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -84,7 +118,6 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -ex
|
set -ex
|
||||||
sudo apt-get update
|
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
|
||||||
sudo apt-get install libgtk+-3.0 gtk+-3.0
|
|
||||||
|
|
||||||
make build
|
make build
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
name: Codecov
|
name: Codecov
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
@ -9,27 +8,26 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
codecov:
|
codecov:
|
||||||
container:
|
container:
|
||||||
image: swift:5.3.2-bionic
|
image: swiftlang/swift:nightly-5.7-focal
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: apt-get update && 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
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Build Test Target
|
- name: Build Test Target
|
||||||
run: swift build --enable-test-discovery -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: swift test --enable-test-discovery --enable-code-coverage --skip-build
|
run: swift test --enable-code-coverage --skip-build
|
||||||
- name: Generate Branch Coverage Report
|
- name: Generate Branch Coverage Report
|
||||||
uses: mattpolzin/swift-codecov-action@0.6.1
|
uses: mattpolzin/swift-codecov-action@0.7.1
|
||||||
id: cov
|
id: cov
|
||||||
with:
|
with:
|
||||||
MINIMUM_COVERAGE: 20
|
MINIMUM_COVERAGE: 15
|
||||||
- name: Post Positive Results
|
- name: Post Positive Results
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
run: |
|
run: |
|
||||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||||
- name: Post Negative Results
|
- name: Post Negative Results
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
run: |
|
run: |
|
||||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ jobs:
|
||||||
dependencies,
|
dependencies,
|
||||||
documentation,
|
documentation,
|
||||||
enhancement,
|
enhancement,
|
||||||
|
Fiber,
|
||||||
refactor,
|
refactor,
|
||||||
SwiftUI compatibility,
|
SwiftUI compatibility,
|
||||||
test suite,
|
test suite,
|
||||||
|
|
|
@ -12,7 +12,13 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
# Fetch current versions of files
|
||||||
|
- name: Fetch base ref
|
||||||
|
run: |
|
||||||
|
git fetch --prune --no-tags --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/heads/${{ github.base_ref }}
|
||||||
|
# Diff pull request to current files, then SwiftLint changed files
|
||||||
- name: GitHub Action for SwiftLint
|
- name: GitHub Action for SwiftLint
|
||||||
uses: norio-nomura/action-swiftlint@3.1.0
|
uses: mayk-it/action-swiftlint@3.2.2
|
||||||
env:
|
env:
|
||||||
DIFF_BASE: ${{ github.base_ref }}
|
DIFF_BASE: ${{ github.base_ref }}
|
||||||
|
DIFF_HEAD: HEAD
|
||||||
|
|
|
@ -41,3 +41,6 @@ Pods/
|
||||||
# SwiftPM
|
# SwiftPM
|
||||||
.build
|
.build
|
||||||
/Packages
|
/Packages
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/launch.json
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
exclude: __Snapshots__
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.5.0
|
rev: v2.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- repo: https://github.com/hodovani/pre-commit-swift
|
- repo: https://github.com/hodovani/pre-commit-swift
|
||||||
rev: master
|
rev: master
|
||||||
hooks:
|
hooks:
|
||||||
- id: swift-lint
|
- id: swift-lint
|
||||||
- id: swift-format
|
- id: swift-format
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
wasm-5.6.0-RELEASE
|
|
@ -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.3
|
--swiftversion 5.6
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"preLaunchTask": "make",
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug",
|
|
||||||
"program": "${workspaceFolder}/.build/debug/TokamakGTKDemo",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
192
CHANGELOG.md
192
CHANGELOG.md
|
@ -1,3 +1,195 @@
|
||||||
|
# 0.10.1 (20 May 2022)
|
||||||
|
|
||||||
|
This is a small bugfix release, which updates JavaScriptKit dependency to 0.15 and required version of `carton` to 0.15.
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Update JSKit dependency ([#482](https://github.com/TokamakUI/Tokamak/pull/482)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Explicitly mention `carton` version in "Requirements" ([#481](https://github.com/TokamakUI/Tokamak/pull/481)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Use stable `v5.6` version of `swiftwasm-action` ([#477](https://github.com/TokamakUI/Tokamak/pull/477)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
|
# 0.10.0 (9 April 2022)
|
||||||
|
|
||||||
|
This release adds support for SwiftWasm 5.6. It also updates JavaScriptKit and OpenCombineJS dependencies.
|
||||||
|
Due to issues with support for older SwiftWasm releases in the `carton`/SwiftPM integration, Tokamak now requires
|
||||||
|
SwiftWasm 5.6 or later, while SwiftWasm 5.4 and 5.5 are no longer supported.
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Build and test with SwiftWasm 5.6 on CI ([#475](https://github.com/TokamakUI/Tokamak/pull/475)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
|
# 0.9.1 (16 February 2022)
|
||||||
|
|
||||||
|
This release fixes an issue with `EnvironmentValues`, updates CI workflow for SwiftWasm 5.5, and bumps JavaScriptKit dependency to 0.12.0.
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Fix typo ([#462](https://github.com/TokamakUI/Tokamak/pull/462)) via [@regexident](https://github.com/regexident)
|
||||||
|
- Fix `rootEnvironment` not merged with `.defaultEnvironment` ([#461](https://github.com/TokamakUI/Tokamak/pull/461)) via [@regexident](https://github.com/regexident)
|
||||||
|
- Build and test with SwiftWasm 5.5 on CI ([#460](https://github.com/TokamakUI/Tokamak/pull/460)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
|
# 0.9.0 (26 November 2021)
|
||||||
|
|
||||||
|
This release adds support for SwiftWasm 5.5 and bumps the minimum required version to Swift 5.4.
|
||||||
|
It now depends on JavaScriptKit 0.11.1, which no longer requires manual memory management of
|
||||||
|
`JSClosure` instances. The downside of that update is that minimum browser version requirements are
|
||||||
|
significantly higher now. See [`README.md`](README.md#requirements) for more details.
|
||||||
|
|
||||||
|
Additionally, a few new features were added to the DOM renderer:
|
||||||
|
|
||||||
|
- `Canvas` and `TimelineView`;
|
||||||
|
- `onHover` modifier;
|
||||||
|
- `task` modifier for running `async` functions;
|
||||||
|
- Sanitizers for `Text` view.
|
||||||
|
|
||||||
|
Many thanks (in alphabetical order) to [@agg23](https://github.com/agg23),
|
||||||
|
[@carson-katri](https://github.com/carson-katri), [@ezraberch](https://github.com/ezraberch),
|
||||||
|
and [@mbrandonw](https://github.com/mbrandonw) for their contributions to this release!
|
||||||
|
|
||||||
|
**Closed issues:**
|
||||||
|
|
||||||
|
- `TextField` Not Rendering the field ([#455](https://github.com/TokamakUI/Tokamak/issues/455))
|
||||||
|
- Can't find `CGSize` or `CGFloat` type ([#450](https://github.com/TokamakUI/Tokamak/issues/450))
|
||||||
|
- `UnitPoint` constants don't match SwiftUI ([#443](https://github.com/TokamakUI/Tokamak/issues/443))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Update for JSKit 0.11.1, add async `task` modifier ([#457](https://github.com/TokamakUI/Tokamak/pull/457)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Switch to Xcode 13.0 in `gtk_macos_build` job ([#454](https://github.com/TokamakUI/Tokamak/pull/454)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add `Canvas` and `TimelineView` to DOM renderer ([#449](https://github.com/TokamakUI/Tokamak/pull/449)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Initial implementation of `onHover` ([#448](https://github.com/TokamakUI/Tokamak/pull/448)) via [@agg23](https://github.com/agg23)
|
||||||
|
- Refactor `NavigationView` ([#446](https://github.com/TokamakUI/Tokamak/pull/446)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Save HTML snapshots with .html extension. ([#447](https://github.com/TokamakUI/Tokamak/pull/447)) via [@mbrandonw](https://github.com/mbrandonw)
|
||||||
|
- Add HTML renderer support for AngularGradient ([#444](https://github.com/TokamakUI/Tokamak/pull/444)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Bump requirements to Swift 5.4, migrate to `@resultBuilder` ([#442](https://github.com/TokamakUI/Tokamak/pull/442)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add HTML sanitizer to `Text` ([#437](https://github.com/TokamakUI/Tokamak/pull/437)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Add `@ezraberch` to the list of maintainers ([#440](https://github.com/TokamakUI/Tokamak/pull/440)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
|
# 0.8.0 (17 August 2021)
|
||||||
|
|
||||||
|
This release adds support for more SwiftUI types and modifiers, and fixes bugs. Including, but not
|
||||||
|
limited to:
|
||||||
|
|
||||||
|
- `Toolbar` type and `toolbar` modifier
|
||||||
|
- `ProgressView` type
|
||||||
|
- `Animation` and related types and modifiers
|
||||||
|
- `opacity`, `scaleEffect`, `aspectRatio`, and `controlSize` modifiers
|
||||||
|
- `Material` and `Gradient` types
|
||||||
|
- `HierarchicalShapeStyle` (`.primary`/`.secondary`/`.tertiary`/`.quaternary`) type
|
||||||
|
- `ContainerRelativeShape` type
|
||||||
|
- `spacing` argument support for initializers of `HStack` and `VStack`
|
||||||
|
- support for standard Foundation types, such as `CGRect`, `CGSize` (we previously used our own
|
||||||
|
implementation of those, which weren't fully compatible with Foundation)
|
||||||
|
- ability to sort HTML attributes when generating static HTML, which is essential for end-to-end
|
||||||
|
tests that cover generated output.
|
||||||
|
|
||||||
|
Many thanks to [@carson-katri](https://github.com/carson-katri),
|
||||||
|
[@ezraberch](https://github.com/ezraberch), and [@yonihemi](https://github.com/yonihemi) for
|
||||||
|
their contributions to this release!
|
||||||
|
|
||||||
|
**Closed issues:**
|
||||||
|
|
||||||
|
- Is there anyway to compile this from Xcode? ([#406](https://github.com/TokamakUI/Tokamak/issues/406))
|
||||||
|
- Xcode doesn't compile — gtk/gtk.h not found ([#405](https://github.com/TokamakUI/Tokamak/issues/405))
|
||||||
|
- Use `NSGeometry` types from Foundation ([#404](https://github.com/TokamakUI/Tokamak/issues/404))
|
||||||
|
- Adding padding to a view contained in a Button causes the Button to disappear ([#403](https://github.com/TokamakUI/Tokamak/issues/403))
|
||||||
|
- .background modifier with contained shape causes view to expand to full vertical size of the screen ([#402](https://github.com/TokamakUI/Tokamak/issues/402))
|
||||||
|
- Multi-line string handling in Text views ([#400](https://github.com/TokamakUI/Tokamak/issues/400))
|
||||||
|
- Content with spacer jumps when blurring and focusing the page ([#395](https://github.com/TokamakUI/Tokamak/issues/395))
|
||||||
|
- Frame sizes do not match expected behavior. ([#387](https://github.com/TokamakUI/Tokamak/issues/387))
|
||||||
|
- URL hash change demo crashes ([#369](https://github.com/TokamakUI/Tokamak/issues/369))
|
||||||
|
- Infinite loops w/ 100% CPU usage caused by stack overflows ([#367](https://github.com/TokamakUI/Tokamak/issues/367))
|
||||||
|
- TokamakDemo breaks after use of `_domRef` ([#326](https://github.com/TokamakUI/Tokamak/issues/326))
|
||||||
|
- Add support for `toolbar` modifier and related types ([#316](https://github.com/TokamakUI/Tokamak/issues/316))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Revise `ShapeStyle` and add `Gradient`s ([#435](https://github.com/TokamakUI/Tokamak/pull/435)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `Toolbar` implementation for HTML renderer ([#169](https://github.com/TokamakUI/Tokamak/pull/169)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Fix SwiftLint action ([#434](https://github.com/TokamakUI/Tokamak/pull/434)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Add View Traits and transitions ([#426](https://github.com/TokamakUI/Tokamak/pull/426)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `ToolbarItem` and its builder functions ([#430](https://github.com/TokamakUI/Tokamak/pull/430)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add `controlSize`/`controlProminence` modifiers ([#431](https://github.com/TokamakUI/Tokamak/pull/431)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Fix background/overlay layout in DOM/HTML renderers ([#429](https://github.com/TokamakUI/Tokamak/pull/429)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `ProgressView` ([#425](https://github.com/TokamakUI/Tokamak/pull/425)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add support for custom fonts ([#421](https://github.com/TokamakUI/Tokamak/pull/421)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Animation implementation using the Web Animations API ([#427](https://github.com/TokamakUI/Tokamak/pull/427)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `scaleEffect` modifier ([#424](https://github.com/TokamakUI/Tokamak/pull/424)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `aspectRatio` modifier ([#422](https://github.com/TokamakUI/Tokamak/pull/422)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Check minWidth/Height == nil ([#420](https://github.com/TokamakUI/Tokamak/pull/420)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add Primary/Secondary/Tertiary/QuaternaryContentStyle ([#419](https://github.com/TokamakUI/Tokamak/pull/419)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `Material` to the HTML renderer ([#418](https://github.com/TokamakUI/Tokamak/pull/418)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Improve ShapeStyles to match iOS 15+ ([#417](https://github.com/TokamakUI/Tokamak/pull/417)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add ContainerRelativeShape ([#416](https://github.com/TokamakUI/Tokamak/pull/416)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add HTML implementation for `opacity` modifier ([#415](https://github.com/TokamakUI/Tokamak/pull/415)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Support `spacing` property on `HStack`/`VStack` ([#273](https://github.com/TokamakUI/Tokamak/pull/273)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Explicitly import CoreFoundation ([#413](https://github.com/TokamakUI/Tokamak/pull/413)) via [@yonihemi](https://github.com/yonihemi)
|
||||||
|
- Fix handling of stroked shapes ([#414](https://github.com/TokamakUI/Tokamak/pull/414)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Add a snapshot test for `Path` SVG layout ([#412](https://github.com/TokamakUI/Tokamak/pull/412)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Attempt `padding` modifier fusion to avoid nested `div`s ([#253](https://github.com/TokamakUI/Tokamak/pull/253)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Use `CGFloat`, `CGPoint`, `CGRect` from Foundation ([#411](https://github.com/TokamakUI/Tokamak/pull/411)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add reconciler stress tests for elaborate testing ([#381](https://github.com/TokamakUI/Tokamak/pull/381)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Fix spacers after `DOMRenderer.update` ([#410](https://github.com/TokamakUI/Tokamak/pull/410)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Replace `ViewDeferredToRenderer`, fix renderer tests ([#408](https://github.com/TokamakUI/Tokamak/pull/408)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Allow DOMRenderer to render buttons with non-Text labels (#403) ([#409](https://github.com/TokamakUI/Tokamak/pull/409)) via [@ezraberch](https://github.com/ezraberch)
|
||||||
|
- Sort attributes in HTML nodes when rendering ([#346](https://github.com/TokamakUI/Tokamak/pull/346)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
|
# 0.7.0 (3 May 2021)
|
||||||
|
|
||||||
|
This release introduces new view types such as `DatePicker`, new modifiers such as `shadow`,
|
||||||
|
improves test coverage, updates dependencies, and fixes multiple bugs and crashes. Additionally,
|
||||||
|
a proof of concept GTK renderer is now available in the `TokamakGTK` module.
|
||||||
|
|
||||||
|
Many thanks to (in alphabetical order)
|
||||||
|
[@carson-katri](https://github.com/carson-katri), [@filip-sakel](https://github.com/filip-sakel),
|
||||||
|
[@foscomputerservices](https://github.com/foscomputerservices), [@literalpie](https://github.com/literalpie),
|
||||||
|
[@mattpolzin](https://github.com/mattpolzin), [@mortenbekditlevsen](https://github.com/mortenbekditlevsen),
|
||||||
|
and [@Snowy1803](https://github.com/Snowy1803) for their contributions to this release!
|
||||||
|
|
||||||
|
**Closed issues:**
|
||||||
|
|
||||||
|
- `@ObservedObject` is a get-only property ([#392](https://github.com/TokamakUI/Tokamak/issues/392))
|
||||||
|
- What is the difference between `HTML` and `DynamicHTML`? ([#388](https://github.com/TokamakUI/Tokamak/issues/388))
|
||||||
|
- Reduce `View.body` Visibility ([#385](https://github.com/TokamakUI/Tokamak/issues/385))
|
||||||
|
- Verify that type constructor names contain contain module names ([#368](https://github.com/TokamakUI/Tokamak/issues/368))
|
||||||
|
- Crash when using a `View` with optional content ([#362](https://github.com/TokamakUI/Tokamak/issues/362))
|
||||||
|
- Set up code coverage reports on GitHub Actions ([#350](https://github.com/TokamakUI/Tokamak/issues/350))
|
||||||
|
- Shadow support ([#324](https://github.com/TokamakUI/Tokamak/issues/324))
|
||||||
|
- Implement `DatePicker` view in the DOM renderer ([#320](https://github.com/TokamakUI/Tokamak/issues/320))
|
||||||
|
- `TokamakDemo` build failed ([#305](https://github.com/TokamakUI/Tokamak/issues/305))
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- Add `@dynamicMemberLookup` to `Binding` ([#396](https://github.com/TokamakUI/Tokamak/pull/396)) via [@carson-katri](https://github.com/carson-katri)
|
||||||
|
- Add `DatePicker` to the `TokamakDOM` module ([#394](https://github.com/TokamakUI/Tokamak/pull/394)) via [@Snowy1803](https://github.com/Snowy1803)
|
||||||
|
- Use `String(reflecting:)` vs `String(describing:)` ([#391](https://github.com/TokamakUI/Tokamak/pull/391)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Clarify the difference between `HTML` and `DynamicHTML` ([#389](https://github.com/TokamakUI/Tokamak/pull/389)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add `_spi(TokamakCore)` to ideally internal public members ([#386](https://github.com/TokamakUI/Tokamak/pull/386)) via [@filip-sakel](https://github.com/filip-sakel)
|
||||||
|
- Make properties of `CGPoint`, `CGSize` and `CGRect` `var`s instead of `let`s ([#382](https://github.com/TokamakUI/Tokamak/pull/382)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||||
|
- Use immediate scheduler in `TestRenderer` ([#380](https://github.com/TokamakUI/Tokamak/pull/380)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Simple Code Coverage analysis ([#378](https://github.com/TokamakUI/Tokamak/pull/378)) via [@mattpolzin](https://github.com/mattpolzin)
|
||||||
|
- Add checks for metadata state ([#375](https://github.com/TokamakUI/Tokamak/pull/375)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Use upstream OpenCombine instead of a fork ([#377](https://github.com/TokamakUI/Tokamak/pull/377)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Update JavaScriptKit, OpenCombineJS dependencies ([#376](https://github.com/TokamakUI/Tokamak/pull/376)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Clean up metadata reflection code ([#372](https://github.com/TokamakUI/Tokamak/pull/372)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add David Hunt to the list of maintainers ([#373](https://github.com/TokamakUI/Tokamak/pull/373)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Refactor environment injection, add a test ([#371](https://github.com/TokamakUI/Tokamak/pull/371)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Replace uses of the Runtime library with stdlib ([#370](https://github.com/TokamakUI/Tokamak/pull/370)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Use `macos-latest` agent for the GTK build ([#360](https://github.com/TokamakUI/Tokamak/pull/360)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add a benchmark target and a script to run it ([#365](https://github.com/TokamakUI/Tokamak/pull/365)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Fix crashes in views with optional content ([#364](https://github.com/TokamakUI/Tokamak/pull/364)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add GTK support for `SecureField` ([#363](https://github.com/TokamakUI/Tokamak/pull/363)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||||
|
- Add support for shadow modifier ([#355](https://github.com/TokamakUI/Tokamak/pull/355)) via [@literalpie](https://github.com/literalpie)
|
||||||
|
- Two infinite loop fixes ([#359](https://github.com/TokamakUI/Tokamak/pull/359)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||||
|
- Added `TextField` support for GTK using `GtkEntry` ([#361](https://github.com/TokamakUI/Tokamak/pull/361)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||||
|
- Fixed a small issue with re-renderers being dropped ([#356](https://github.com/TokamakUI/Tokamak/pull/356)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||||
|
- Removed an extra space that cause Safari to issue "Invalid value" ([#358](https://github.com/TokamakUI/Tokamak/pull/358)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||||
|
- Add `@mortenbekditlevsen` to the list of maintainers in `README.md` ([#352](https://github.com/TokamakUI/Tokamak/pull/352)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Build the GTK renderer on Ubuntu on CI ([#347](https://github.com/TokamakUI/Tokamak/pull/347)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- Add missing `Link` re-export to TokamakDOM ([#351](https://github.com/TokamakUI/Tokamak/pull/351)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
- GTK shape support WIP ([#348](https://github.com/TokamakUI/Tokamak/pull/348)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||||
|
- Add a "bug report" issue template ([#349](https://github.com/TokamakUI/Tokamak/pull/349)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||||
|
|
||||||
# 0.6.1 (6 December 2020)
|
# 0.6.1 (6 December 2020)
|
||||||
|
|
||||||
This release fixes autocomplete in Xcode for projects that depend on Tokamak.
|
This release fixes autocomplete in Xcode for projects that depend on Tokamak.
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
### Modular structure
|
||||||
|
|
||||||
|
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
|
||||||
|
separate modules for platform-specific renderers. Currently, the only available renderer modules are
|
||||||
|
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
|
||||||
|
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers
|
||||||
|
guide](docs/RenderersGuide.md) for more details.
|
||||||
|
|
||||||
|
Tokamak users only need to import a renderer module they would like to use, while
|
||||||
|
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
|
||||||
|
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
|
||||||
|
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
|
||||||
|
those symbols with underscores in their names to indicate this. It can be formulated as these
|
||||||
|
"rules":
|
||||||
|
|
||||||
|
1. If a symbol is restricted to a module and has no `public` access control, no need for an
|
||||||
|
underscore.
|
||||||
|
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
|
||||||
|
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
|
||||||
|
renderer module via `public typealias`.
|
||||||
|
3. If a function or a type have `public` on them only by necessity to make them available in
|
||||||
|
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
|
||||||
|
indicate that.
|
||||||
|
|
||||||
|
The benefit of separate modules is that they allow us to provide separate renderers for different
|
||||||
|
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
|
||||||
|
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
|
||||||
|
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
|
||||||
|
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
|
||||||
|
`TokamakAndroid` with no need to be aware of any of the web modules.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Tokamak uses [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library to
|
||||||
|
make sure that HTML and layout generated by renderers stay consistent. To run the test suite on macOS
|
||||||
|
you should use this command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, plain `swift test` won't work as it tries to build targets that aren't related to
|
||||||
|
the test suite.
|
||||||
|
|
||||||
|
### Coding Style
|
||||||
|
|
||||||
|
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
||||||
|
[SwiftLint](https://github.com/realm/SwiftLint) to enforce formatting and coding style. SwiftFormat
|
||||||
|
0.45.3 and SwiftLint 0.39.2 or later versions are recommended. We encourage you to run SwiftFormat
|
||||||
|
and SwiftLint within a local clone of the repository in whatever way works best for you. You can do
|
||||||
|
that either manually, or automatically with VSCode extensions for
|
||||||
|
[SwiftFormat](https://github.com/vknabel/vscode-swiftformat) and
|
||||||
|
[SwiftLint](https://github.com/vknabel/vscode-swiftlint) respectively, or with the [Xcode
|
||||||
|
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension), or [build
|
||||||
|
phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase).
|
||||||
|
|
||||||
|
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run
|
||||||
|
this once to set up the [pre-commit](https://pre-commit.com/) hook:
|
||||||
|
|
||||||
|
```
|
||||||
|
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
|
||||||
|
pre-commit install # installs pre-commit hook to run checks before you commit
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
|
||||||
|
and installation instructions for other platforms.
|
||||||
|
|
||||||
|
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
|
||||||
|
fail with inconsistent formatting or style. We require CI builds to pass for all
|
||||||
|
PRs before merging.
|
2
LICENSE
2
LICENSE
|
@ -186,7 +186,7 @@ file or class name and description of purpose be included on the
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2018-2020 Digital Signal Limited and Tokamak Contributors
|
Copyright 2018-2021 Digital Signal Limited and Tokamak Contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -7,59 +7,75 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
|
|
||||||
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
|
|
||||||
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
|
|
||||||
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
|
|
||||||
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
|
|
||||||
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
|
|
||||||
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
|
|
||||||
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
|
|
||||||
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; };
|
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; };
|
||||||
85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.storyboard */; };
|
85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.storyboard */; };
|
||||||
85ED188C24AD3CF10085DFA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */; };
|
85ED188C24AD3CF10085DFA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */; };
|
||||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
|
|
||||||
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
|
|
||||||
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
|
|
||||||
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
|
|
||||||
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
|
|
||||||
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
|
|
||||||
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
|
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
|
||||||
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
|
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
|
||||||
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
|
|
||||||
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
|
|
||||||
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
|
|
||||||
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
|
|
||||||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
|
|
||||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
|
|
||||||
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
|
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
|
||||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
|
||||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
|
||||||
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
|
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
|
||||||
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
|
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
|
||||||
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
|
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
|
||||||
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
|
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
|
||||||
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
|
||||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
|
||||||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
|
||||||
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
|
||||||
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
|
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
|
||||||
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
|
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
|
||||||
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; };
|
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
|
||||||
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; };
|
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
|
||||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
|
||||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
|
||||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
|
||||||
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
|
||||||
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
|
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
|
||||||
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
|
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
|
||||||
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
|
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
|
||||||
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
|
D1078763274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
|
||||||
|
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
|
||||||
|
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
|
||||||
|
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
|
||||||
|
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
|
||||||
|
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
|
||||||
|
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
|
||||||
|
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
|
||||||
|
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
|
||||||
|
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
|
||||||
|
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
|
||||||
|
D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
|
||||||
|
D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
|
||||||
|
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
|
||||||
|
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
|
||||||
|
D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
|
||||||
|
D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
|
||||||
|
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
|
||||||
|
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
|
||||||
|
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
|
||||||
|
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
|
||||||
|
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
|
||||||
|
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
|
||||||
|
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
|
||||||
|
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
|
||||||
|
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
|
||||||
|
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
|
||||||
|
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
|
||||||
|
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
|
||||||
|
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
|
||||||
|
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
|
||||||
|
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
|
||||||
|
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
|
||||||
|
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
|
||||||
|
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
|
||||||
|
D107878A274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
|
||||||
|
D107878B274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
|
||||||
|
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
|
||||||
|
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
|
||||||
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
|
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
|
||||||
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
||||||
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
||||||
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
|
|
||||||
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -92,40 +108,48 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
|
|
||||||
4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
|
|
||||||
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
|
|
||||||
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
|
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
|
||||||
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
85ED185224AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
85ED185224AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAppDelegate.swift; sourceTree = "<group>"; };
|
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAppDelegate.swift; sourceTree = "<group>"; };
|
||||||
85ED188724AD3CC30085DFA0 /* macOS.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = macOS.storyboard; sourceTree = "<group>"; };
|
85ED188724AD3CC30085DFA0 /* macOS.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = macOS.storyboard; sourceTree = "<group>"; };
|
||||||
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAppDelegate.swift; sourceTree = "<group>"; };
|
85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAppDelegate.swift; sourceTree = "<group>"; };
|
||||||
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
|
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
|
||||||
85ED189B24AD425E0085DFA0 /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED189E24AD425E0085DFA0 /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
|
|
||||||
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
|
|
||||||
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
|
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
|
||||||
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
|
D1078726274BD1E5003E787B /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
|
||||||
B56F22DF24BC89FD001738DF /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
|
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
|
||||||
B56F22E224BD1C26001738DF /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
|
D1078728274BD1E5003E787B /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
|
||||||
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
|
D1078729274BD1E5003E787B /* StackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackDemo.swift; sourceTree = "<group>"; };
|
||||||
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
|
D107872B274BD1E5003E787B /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
|
||||||
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
|
D107872C274BD1E5003E787B /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
|
||||||
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; };
|
D107872D274BD1E5003E787B /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
|
||||||
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
|
D107872E274BD1E5003E787B /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
|
||||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
|
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
|
||||||
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
|
D1078731274BD1E5003E787B /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
|
||||||
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
|
D1078732274BD1E5003E787B /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078733274BD1E5003E787B /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078735274BD1E5003E787B /* TaskDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078736274BD1E5003E787B /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107873B274BD1E5003E787B /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107873C274BD1E5003E787B /* CanvasDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107873D274BD1E5003E787B /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078740274BD1E5003E787B /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078742274BD1E5003E787B /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078744274BD1E5003E787B /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078746274BD1E5003E787B /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078748274BD1E5003E787B /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D1078749274BD1E5003E787B /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107874A274BD1E5003E787B /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
|
||||||
|
D107874C274BD1E5003E787B /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
|
||||||
|
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
|
||||||
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
|
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
|
||||||
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -185,34 +209,106 @@
|
||||||
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
|
D107874B274BD1E5003E787B /* Buttons */,
|
||||||
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */,
|
D107872F274BD1E5003E787B /* Containers */,
|
||||||
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
|
D107873A274BD1E5003E787B /* Drawing */,
|
||||||
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
|
D1078725274BD1E5003E787B /* Layout */,
|
||||||
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
|
D107873F274BD1E5003E787B /* Misc */,
|
||||||
85ED189E24AD425E0085DFA0 /* Counter.swift */,
|
D1078734274BD1E5003E787B /* Modifiers */,
|
||||||
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */,
|
D107872A274BD1E5003E787B /* Selectors */,
|
||||||
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */,
|
D1078747274BD1E5003E787B /* Text */,
|
||||||
B56F22E224BD1C26001738DF /* GridDemo.swift */,
|
|
||||||
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */,
|
|
||||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
|
|
||||||
B51F214F24B920B400CF2583 /* PathDemo.swift */,
|
|
||||||
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
|
|
||||||
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */,
|
|
||||||
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
|
|
||||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
|
|
||||||
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */,
|
|
||||||
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */,
|
|
||||||
85ED189B24AD425E0085DFA0 /* TextDemo.swift */,
|
|
||||||
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
|
|
||||||
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */,
|
|
||||||
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */,
|
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */,
|
||||||
4550BD5125B642B80088F4EA /* ShadowDemo.swift */,
|
|
||||||
);
|
);
|
||||||
name = TokamakDemo;
|
name = TokamakDemo;
|
||||||
path = ../Sources/TokamakDemo;
|
path = ../Sources/TokamakDemo;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D1078725274BD1E5003E787B /* Layout */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D1078726274BD1E5003E787B /* SpacerDemo.swift */,
|
||||||
|
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */,
|
||||||
|
D1078728274BD1E5003E787B /* GridDemo.swift */,
|
||||||
|
D1078729274BD1E5003E787B /* StackDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Layout;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D107872A274BD1E5003E787B /* Selectors */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D107872B274BD1E5003E787B /* DatePickerDemo.swift */,
|
||||||
|
D107872C274BD1E5003E787B /* SliderDemo.swift */,
|
||||||
|
D107872D274BD1E5003E787B /* PickerDemo.swift */,
|
||||||
|
D107872E274BD1E5003E787B /* ToggleDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Selectors;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D107872F274BD1E5003E787B /* Containers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */,
|
||||||
|
D1078731274BD1E5003E787B /* ForEachDemo.swift */,
|
||||||
|
D1078732274BD1E5003E787B /* ListDemo.swift */,
|
||||||
|
D1078733274BD1E5003E787B /* SidebarDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Containers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D1078734274BD1E5003E787B /* Modifiers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D1078735274BD1E5003E787B /* TaskDemo.swift */,
|
||||||
|
D1078736274BD1E5003E787B /* ShadowDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Modifiers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D107873A274BD1E5003E787B /* Drawing */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D107873B274BD1E5003E787B /* PathDemo.swift */,
|
||||||
|
D107873C274BD1E5003E787B /* CanvasDemo.swift */,
|
||||||
|
D107873D274BD1E5003E787B /* ColorDemo.swift */,
|
||||||
|
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Drawing;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D107873F274BD1E5003E787B /* Misc */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D1078740274BD1E5003E787B /* AnimationDemo.swift */,
|
||||||
|
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */,
|
||||||
|
D1078742274BD1E5003E787B /* TransitionDemo.swift */,
|
||||||
|
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */,
|
||||||
|
D1078744274BD1E5003E787B /* AppStorageDemo.swift */,
|
||||||
|
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */,
|
||||||
|
D1078746274BD1E5003E787B /* RedactDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Misc;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D1078747274BD1E5003E787B /* Text */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D1078748274BD1E5003E787B /* TextDemo.swift */,
|
||||||
|
D1078749274BD1E5003E787B /* TextEditorDemo.swift */,
|
||||||
|
D107874A274BD1E5003E787B /* TextFieldDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Text;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D107874B274BD1E5003E787B /* Buttons */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D107874C274BD1E5003E787B /* Counter.swift */,
|
||||||
|
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */,
|
||||||
|
);
|
||||||
|
path = Buttons;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D1E5FDAB24C1D57000E7485E /* TokamakShim */ = {
|
D1E5FDAB24C1D57000E7485E /* TokamakShim */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -351,30 +447,38 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */,
|
||||||
|
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */,
|
||||||
|
D107878A274BD1E5003E787B /* Counter.swift in Sources */,
|
||||||
|
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */,
|
||||||
|
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
|
||||||
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
|
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
|
||||||
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */,
|
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
|
||||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
|
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */,
|
||||||
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
|
|
||||||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
|
||||||
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
|
|
||||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
|
|
||||||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
|
||||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
|
||||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
|
||||||
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
|
||||||
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
|
|
||||||
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
|
||||||
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
|
||||||
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */,
|
|
||||||
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
||||||
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */,
|
||||||
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */,
|
||||||
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */,
|
||||||
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
|
||||||
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
|
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */,
|
||||||
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */,
|
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
|
||||||
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */,
|
||||||
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
|
D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */,
|
||||||
|
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
|
||||||
|
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
|
||||||
|
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */,
|
||||||
|
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */,
|
||||||
|
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */,
|
||||||
|
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
|
||||||
|
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */,
|
||||||
|
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */,
|
||||||
|
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */,
|
||||||
|
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
|
||||||
|
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */,
|
||||||
|
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
|
||||||
|
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
|
||||||
|
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */,
|
||||||
|
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -382,30 +486,38 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */,
|
||||||
|
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */,
|
||||||
|
D107878B274BD1E5003E787B /* Counter.swift in Sources */,
|
||||||
|
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */,
|
||||||
|
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
|
||||||
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
||||||
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */,
|
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
|
||||||
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
|
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */,
|
||||||
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
|
|
||||||
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
|
||||||
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
|
|
||||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
|
|
||||||
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
|
||||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
|
||||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
|
||||||
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
|
||||||
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
|
|
||||||
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
|
||||||
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
|
||||||
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */,
|
|
||||||
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
|
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
|
||||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */,
|
||||||
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */,
|
||||||
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
|
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */,
|
||||||
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
|
||||||
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
|
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */,
|
||||||
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */,
|
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
|
||||||
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */,
|
||||||
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */,
|
||||||
|
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
|
||||||
|
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
|
||||||
|
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */,
|
||||||
|
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */,
|
||||||
|
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */,
|
||||||
|
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
|
||||||
|
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */,
|
||||||
|
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */,
|
||||||
|
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */,
|
||||||
|
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
|
||||||
|
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */,
|
||||||
|
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
|
||||||
|
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
|
||||||
|
D1078763274BD1E5003E787B /* ListDemo.swift in Sources */,
|
||||||
|
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
105
Package.resolved
105
Package.resolved
|
@ -1,52 +1,59 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"pins" : [
|
||||||
"pins": [
|
{
|
||||||
{
|
"identity" : "javascriptkit",
|
||||||
"package": "JavaScriptKit",
|
"kind" : "remoteSourceControl",
|
||||||
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git",
|
"location" : "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||||
"state": {
|
"state" : {
|
||||||
"branch": null,
|
"revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
|
||||||
"revision": "ebd9ca04215397f0e3cb72d6e96406a980a424e5",
|
"version" : "0.15.0"
|
||||||
"version": "0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "OpenCombine",
|
|
||||||
"repositoryURL": "https://github.com/OpenCombine/OpenCombine.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "28993ae57de5a4ea7e164787636cafad442d568c",
|
|
||||||
"version": "0.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "OpenCombineJS",
|
|
||||||
"repositoryURL": "https://github.com/swiftwasm/OpenCombineJS.git",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "eaf324ce78710f53b52fb82e9a8de4693633e33a",
|
|
||||||
"version": "0.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "swift-argument-parser",
|
|
||||||
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
|
|
||||||
"version": "0.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "Benchmark",
|
|
||||||
"repositoryURL": "https://github.com/google/swift-benchmark",
|
|
||||||
"state": {
|
|
||||||
"branch": null,
|
|
||||||
"revision": "8e0ef8bb7482ab97dcd2cd1d6855bd38921c345d",
|
|
||||||
"version": "0.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
"version": 1
|
"identity" : "opencombine",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/OpenCombine/OpenCombine.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "9cf67e363738dbab61b47fb5eaed78d3db31e5ee",
|
||||||
|
"version" : "0.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "opencombinejs",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "e574e418ba468ff5c2d4c499eb56f108aeb4d2ba",
|
||||||
|
"version" : "0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-argument-parser",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-argument-parser",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
|
||||||
|
"version" : "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-benchmark",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/google/swift-benchmark",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
|
||||||
|
"version" : "0.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-snapshot-testing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f8a9c997c3c1dab4e216a8ec9014e23144cbab37",
|
||||||
|
"version" : "1.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|
133
Package.swift
133
Package.swift
|
@ -1,6 +1,4 @@
|
||||||
// swift-tools-version:5.3
|
// swift-tools-version:5.6
|
||||||
// The swift-tools-version declares the minimum version of Swift required to
|
|
||||||
// build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
|
@ -26,8 +24,8 @@ let package = Package(
|
||||||
targets: ["TokamakStaticHTML"]
|
targets: ["TokamakStaticHTML"]
|
||||||
),
|
),
|
||||||
.executable(
|
.executable(
|
||||||
name: "TokamakStaticDemo",
|
name: "TokamakStaticHTMLDemo",
|
||||||
targets: ["TokamakStaticDemo"]
|
targets: ["TokamakStaticHTMLDemo"]
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "TokamakGTK",
|
name: "TokamakGTK",
|
||||||
|
@ -47,32 +45,40 @@ let package = Package(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
|
||||||
// .package(url: /* package url */, from: "1.0.0"),
|
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/swiftwasm/JavaScriptKit.git",
|
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
|
||||||
.upToNextMinor(from: "0.10.0")
|
from: "0.15.0"
|
||||||
|
),
|
||||||
|
.package(
|
||||||
|
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
|
||||||
|
from: "0.12.0"
|
||||||
|
),
|
||||||
|
.package(
|
||||||
|
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
|
||||||
|
from: "0.2.0"
|
||||||
|
),
|
||||||
|
.package(
|
||||||
|
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
|
||||||
|
from: "0.1.2"
|
||||||
|
),
|
||||||
|
.package(
|
||||||
|
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
|
||||||
|
from: "1.9.0"
|
||||||
),
|
),
|
||||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0"),
|
|
||||||
.package(url: "https://github.com/swiftwasm/OpenCombineJS.git", .upToNextMinor(from: "0.1.1")),
|
|
||||||
.package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.0"),
|
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define
|
// Targets are the basic building blocks of a package. A target can define
|
||||||
// a module or a test suite.
|
// a module or a test suite.
|
||||||
// Targets can depend on other targets in this package, and on products
|
// Targets can depend on other targets in this package, and on products
|
||||||
// in packages which this package depends on.
|
// in packages which this package depends on.
|
||||||
.target(
|
|
||||||
name: "CombineShim",
|
|
||||||
dependencies: [.product(
|
|
||||||
name: "OpenCombine",
|
|
||||||
package: "OpenCombine",
|
|
||||||
condition: .when(platforms: [.wasi, .linux])
|
|
||||||
)]
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakCore",
|
name: "TokamakCore",
|
||||||
dependencies: ["CombineShim"]
|
dependencies: [
|
||||||
|
.product(
|
||||||
|
name: "OpenCombineShim",
|
||||||
|
package: "OpenCombine"
|
||||||
|
),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakShim",
|
name: "TokamakShim",
|
||||||
|
@ -105,9 +111,15 @@ let package = Package(
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakGTK",
|
name: "TokamakGTK",
|
||||||
dependencies: ["TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers", "CombineShim"]
|
dependencies: [
|
||||||
|
"TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers",
|
||||||
|
.product(
|
||||||
|
name: "OpenCombineShim",
|
||||||
|
package: "OpenCombine"
|
||||||
|
),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.executableTarget(
|
||||||
name: "TokamakGTKDemo",
|
name: "TokamakGTKDemo",
|
||||||
dependencies: ["TokamakGTK"],
|
dependencies: ["TokamakGTK"],
|
||||||
resources: [.copy("logo-header.png")]
|
resources: [.copy("logo-header.png")]
|
||||||
|
@ -118,35 +130,44 @@ let package = Package(
|
||||||
"TokamakCore",
|
"TokamakCore",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.executableTarget(
|
||||||
name: "TokamakCoreBenchmark",
|
name: "TokamakCoreBenchmark",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Benchmark",
|
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||||
"TokamakCore",
|
"TokamakCore",
|
||||||
|
"TokamakTestRenderer",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.executableTarget(
|
||||||
name: "TokamakStaticHTMLBenchmark",
|
name: "TokamakStaticHTMLBenchmark",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Benchmark",
|
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||||
"TokamakStaticHTML",
|
"TokamakStaticHTML",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "TokamakDOM",
|
name: "TokamakDOM",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CombineShim",
|
|
||||||
"OpenCombineJS",
|
|
||||||
"TokamakCore",
|
"TokamakCore",
|
||||||
"TokamakStaticHTML",
|
"TokamakStaticHTML",
|
||||||
|
.product(
|
||||||
|
name: "OpenCombineShim",
|
||||||
|
package: "OpenCombine"
|
||||||
|
),
|
||||||
.product(
|
.product(
|
||||||
name: "JavaScriptKit",
|
name: "JavaScriptKit",
|
||||||
package: "JavaScriptKit",
|
package: "JavaScriptKit",
|
||||||
condition: .when(platforms: [.wasi])
|
condition: .when(platforms: [.wasi])
|
||||||
),
|
),
|
||||||
|
.product(
|
||||||
|
name: "JavaScriptEventLoop",
|
||||||
|
package: "JavaScriptKit",
|
||||||
|
condition: .when(platforms: [.wasi])
|
||||||
|
),
|
||||||
|
"OpenCombineJS",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.executableTarget(
|
||||||
name: "TokamakDemo",
|
name: "TokamakDemo",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"TokamakShim",
|
"TokamakShim",
|
||||||
|
@ -156,10 +177,16 @@ let package = Package(
|
||||||
condition: .when(platforms: [.wasi])
|
condition: .when(platforms: [.wasi])
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
resources: [.copy("logo-header.png")]
|
resources: [.copy("logo-header.png")],
|
||||||
|
linkerSettings: [
|
||||||
|
.unsafeFlags(
|
||||||
|
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
|
||||||
|
.when(platforms: [.wasi])
|
||||||
|
),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.executableTarget(
|
||||||
name: "TokamakStaticDemo",
|
name: "TokamakStaticHTMLDemo",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"TokamakStaticHTML",
|
"TokamakStaticHTML",
|
||||||
]
|
]
|
||||||
|
@ -168,18 +195,40 @@ 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"]
|
||||||
),
|
),
|
||||||
// FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved
|
.testTarget(
|
||||||
// Currently, when multiple modules that have conflicting `ViewDeferredToRenderer`
|
name: "TokamakStaticHTMLTests",
|
||||||
// implementations are linked in the same binary, only a single one is used with no defined
|
dependencies: [
|
||||||
// behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution
|
"TokamakStaticHTML",
|
||||||
// that isn't prone to these hard to debug errors.
|
.product(
|
||||||
// .testTarget(
|
name: "SnapshotTesting",
|
||||||
// name: "TokamakStaticHTMLTests",
|
package: "swift-snapshot-testing",
|
||||||
// dependencies: ["TokamakStaticHTML"]
|
condition: .when(platforms: [.macOS])
|
||||||
// ),
|
),
|
||||||
|
],
|
||||||
|
exclude: ["__Snapshots__", "RenderingTests/__Snapshots__"]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
191
README.md
191
README.md
|
@ -52,6 +52,7 @@ struct Counter: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
struct CounterApp: App {
|
struct CounterApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup("Counter Demo") {
|
WindowGroup("Counter Demo") {
|
||||||
|
@ -59,10 +60,6 @@ struct CounterApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @main attribute is not supported in SwiftPM apps.
|
|
||||||
// See https://bugs.swift.org/browse/SR-12683 for more details.
|
|
||||||
CounterApp.main()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arbitrary HTML
|
### Arbitrary HTML
|
||||||
|
@ -82,6 +79,40 @@ struct SVGCircle: View {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`HTML` doesn't support event listeners, and is declared in the `TokamakStaticHTML` module, which `TokamakDOM` re-exports. The benefit of `HTML` is that you can use it for static rendering in libraries like [TokamakVapor](https://github.com/TokamakUI/TokamakVapor) and [TokamakPublish](https://github.com/TokamakUI/TokamakPublish).
|
||||||
|
|
||||||
|
Another option is the `DynamicHTML` view provided by the `TokamakDOM` module, which has a `listeners` property with a corresponding initializer parameter. You can pass closures that can handle `onclick`, `onmouseover` and other DOM events for you in the `listeners` dictionary. Check out [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers) for the full list.
|
||||||
|
|
||||||
|
An example of mouse events handling with `DynamicHTML` would look like this:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct MouseEventsView: View {
|
||||||
|
@State var position: CGPoint = .zero
|
||||||
|
@State var isMouseButtonDown: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
DynamicHTML(
|
||||||
|
"div",
|
||||||
|
["style": "width: 200px; height: 200px; background-color: red;"],
|
||||||
|
listeners: [
|
||||||
|
"mousemove": { event in
|
||||||
|
guard
|
||||||
|
let x = event.offsetX.jsValue.number,
|
||||||
|
let y = event.offsetY.jsValue.number
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
position = CGPoint(x: x, y: y)
|
||||||
|
},
|
||||||
|
"mousedown": { _ in isMouseButtonDown = true },
|
||||||
|
"mouseup": { _ in isMouseButtonDown = false },
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Arbitrary styles and scripts
|
### Arbitrary styles and scripts
|
||||||
|
|
||||||
While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript,
|
While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript,
|
||||||
|
@ -107,23 +138,63 @@ 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.
|
||||||
|
|
||||||
## Requirements for app developers
|
### Fiber renderers
|
||||||
|
|
||||||
- macOS 10.15 and Xcode 11.4 or later. macOS 11.0 and Xcode 12.0 or later are required if you're
|
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
||||||
building a multi-platform app with Tokamak that also needs to support SwiftUI on macOS.
|
is optionally available. It can provide faster updates and allow for larger View hierarchies.
|
||||||
- [Swift 5.2 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use Linux.
|
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
|
||||||
|
|
||||||
|
You can specify which reconciler to use in your `App`'s configuration:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct CounterApp: App {
|
||||||
|
static let _configuration: _AppConfiguration = .init(
|
||||||
|
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
|
||||||
|
reconciler: .fiber(useDynamicLayout: true)
|
||||||
|
)
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup("Counter Demo") {
|
||||||
|
Counter(count: 5, limit: 15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### For app developers
|
||||||
|
|
||||||
|
- macOS 11 and Xcode 13.2 or later when using VS Code. macOS 12 and Xcode 13.3 or later are recommended if
|
||||||
|
you'd like to use Xcode for auto-completion, or when developing multi-platform apps that target WebAssembly
|
||||||
|
and macOS at the same time.
|
||||||
|
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
|
||||||
Other Linux distributions are currently not supported.
|
Other Linux distributions are currently not supported.
|
||||||
|
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
||||||
|
|
||||||
## Requirements for app users
|
### For users of apps depending on Tokamak
|
||||||
|
|
||||||
Any browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) should work, which currently includes:
|
Any recent browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) and [required
|
||||||
|
JavaScript features](https://caniuse.com/?search=finalizationregistry) should work, which currently includes:
|
||||||
|
|
||||||
|
- Edge 84+
|
||||||
|
- Firefox 79+
|
||||||
|
- Chrome 84+
|
||||||
|
- Desktop Safari 14.1+
|
||||||
|
- Mobile Safari 14.8+
|
||||||
|
|
||||||
|
If you need to support older browser versions, you'll have to build with
|
||||||
|
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags
|
||||||
|
when compiling. This should lower browser requirements to these versions:
|
||||||
|
|
||||||
- Edge 16+
|
- Edge 16+
|
||||||
- Firefox 53+
|
- Firefox 61+
|
||||||
- Chrome 57+
|
- Chrome 66+
|
||||||
- (Mobile) Safari 11+
|
- (Mobile) Safari 12+
|
||||||
|
|
||||||
Not all of these were tested though, compatibility reports are very welcome!
|
Not all of these versions are tested on regular basis though, compatibility reports are very welcome!
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
|
@ -138,7 +209,7 @@ app by following these steps:
|
||||||
brew install swiftwasm/tap/carton
|
brew install swiftwasm/tap/carton
|
||||||
```
|
```
|
||||||
|
|
||||||
If you had `carton` installed before this, make sure you have version 0.9.0 or greater:
|
If you had `carton` installed before this, make sure you have version 0.15.0 or greater:
|
||||||
|
|
||||||
```
|
```
|
||||||
carton --version
|
carton --version
|
||||||
|
@ -170,6 +241,26 @@ 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
|
||||||
|
|
||||||
|
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
||||||
|
to override this functionality, you can use the `_domTextSanitizer` modifier:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Text("<font color='red'>Unsanitized Text</font>")
|
||||||
|
._domTextSanitizer(Sanitizers.HTML.insecure)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use custom sanitizers; the argument to `_domTextSanitizer` is simply a
|
||||||
|
`String -> String` closure. If `_domTextSanitizer` is applied to a non-`Text` view,
|
||||||
|
it will apply to all `Text` in subviews, unless overridden.
|
||||||
|
|
||||||
|
If you use user-generated or otherwise unsafe strings elsewhere, make sure to properly
|
||||||
|
sanitize them yourself.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### `unable to find utility "xctest"` error when building
|
### `unable to find utility "xctest"` error when building
|
||||||
|
@ -205,63 +296,14 @@ doesn't provide an official build of the extension on the VSCode Marketplace unf
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
### Modular structure
|
All contributions, no matter how small, are very welcome. You don't have to be a web developer or a
|
||||||
|
SwiftUI expert to meaningfully contribute. In fact, by checking out how some of the simplest views are
|
||||||
|
implemented in Tokamak you may learn more how SwiftUI may work under the hood.
|
||||||
|
|
||||||
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
|
Updating our [documentation](https://github.com/TokamakUI/Tokamak/tree/main/docs) and taking on [the starter
|
||||||
separate modules for platform-specific renderers. Currently, the only available renderer modules are
|
bugs](https://github.com/TokamakUI/Tokamak/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||||
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
|
is also appreciated. Don't forget to join our [Discord server](https://discord.gg/ashJW8T8yp) to get in
|
||||||
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers
|
touch with the maintainers and other users. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.
|
||||||
guide](docs/RenderersGuide.md) for more details.
|
|
||||||
|
|
||||||
Tokamak users only need to import a renderer module they would like to use, while
|
|
||||||
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
|
|
||||||
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
|
|
||||||
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
|
|
||||||
those symbols with underscores in their names to indicate this. It can be formulated as these
|
|
||||||
"rules":
|
|
||||||
|
|
||||||
1. If a symbol is restricted to a module and has no `public` access control, no need for an
|
|
||||||
underscore.
|
|
||||||
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
|
|
||||||
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
|
|
||||||
renderer module via `public typealias`.
|
|
||||||
3. If a function or a type have `public` on them only by necessity to make them available in
|
|
||||||
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
|
|
||||||
indicate that.
|
|
||||||
|
|
||||||
The benefit of separate modules is that they allow us to provide separate renderers for different
|
|
||||||
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
|
|
||||||
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
|
|
||||||
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Coding Style
|
|
||||||
|
|
||||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
|
||||||
[SwiftLint](https://github.com/realm/SwiftLint) to enforce formatting and coding style. SwiftFormat
|
|
||||||
0.45.3 and SwiftLint 0.39.2 or later versions are recommended. We encourage you to run SwiftFormat
|
|
||||||
and SwiftLint within a local clone of the repository in whatever way works best for you. You can do
|
|
||||||
that either manually, or automatically with VSCode extensions for
|
|
||||||
[SwiftFormat](https://github.com/vknabel/vscode-swiftformat) and
|
|
||||||
[SwiftLint](https://github.com/vknabel/vscode-swiftlint) respectively, or with the [Xcode
|
|
||||||
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension), or [build
|
|
||||||
phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase).
|
|
||||||
|
|
||||||
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run
|
|
||||||
this once to set up the [pre-commit](https://pre-commit.com/) hook:
|
|
||||||
|
|
||||||
```
|
|
||||||
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
|
|
||||||
pre-commit install # installs pre-commit hook to run checks before you commit
|
|
||||||
```
|
|
||||||
|
|
||||||
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
|
|
||||||
and installation instructions for other platforms.
|
|
||||||
|
|
||||||
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
|
|
||||||
fail with inconsistent formatting or style. We require CI builds to pass for all
|
|
||||||
PRs before merging.
|
|
||||||
|
|
||||||
### Code of Conduct
|
### Code of Conduct
|
||||||
|
|
||||||
|
@ -283,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),
|
||||||
[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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol Animatable {
|
||||||
|
associatedtype AnimatableData: VectorArithmetic
|
||||||
|
var animatableData: Self.AnimatableData { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol _PrimitiveAnimatable {}
|
||||||
|
|
||||||
|
public extension Animatable where Self: VectorArithmetic {
|
||||||
|
var animatableData: Self {
|
||||||
|
get { self }
|
||||||
|
// swiftlint:disable:next unused_setter_value
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
|
var animatableData: EmptyAnimatableData {
|
||||||
|
@inlinable get { EmptyAnimatableData() }
|
||||||
|
// swiftlint:disable:next unused_setter_value
|
||||||
|
@inlinable set {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct EmptyAnimatableData: VectorArithmetic {
|
||||||
|
@inlinable
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public static var zero: Self { .init() }
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public static func += (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public static func -= (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
|
.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public static func - (lhs: Self, rhs: Self) -> Self {
|
||||||
|
.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public mutating func scale(by rhs: Double) {}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public var magnitudeSquared: Double { .zero }
|
||||||
|
|
||||||
|
public static func == (a: Self, b: Self) -> Bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||||
|
where First: VectorArithmetic, Second: VectorArithmetic
|
||||||
|
{
|
||||||
|
public var first: First
|
||||||
|
public var second: Second
|
||||||
|
@inlinable
|
||||||
|
public init(_ first: First, _ second: Second) {
|
||||||
|
self.first = first
|
||||||
|
self.second = second
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
internal subscript() -> (First, Second) {
|
||||||
|
get { (first, second) }
|
||||||
|
set { (first, second) = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public static var zero: Self {
|
||||||
|
@_transparent get {
|
||||||
|
.init(First.zero, Second.zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public static func += (lhs: inout Self, rhs: Self) {
|
||||||
|
lhs.first += rhs.first
|
||||||
|
lhs.second += rhs.second
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public static func -= (lhs: inout Self, rhs: Self) {
|
||||||
|
lhs.first -= rhs.first
|
||||||
|
lhs.second -= rhs.second
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
|
.init(lhs.first + rhs.first, lhs.second + rhs.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public static func - (lhs: Self, rhs: Self) -> Self {
|
||||||
|
.init(lhs.first - rhs.first, lhs.second - rhs.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public mutating func scale(by rhs: Double) {
|
||||||
|
first.scale(by: rhs)
|
||||||
|
second.scale(by: rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
|
@_transparent get {
|
||||||
|
first.magnitudeSquared + second.magnitudeSquared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (a: Self, b: Self) -> Bool {
|
||||||
|
a.first == b.first
|
||||||
|
&& a.second == b.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGPoint: Animatable {
|
||||||
|
public var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||||
|
@inlinable get { .init(x, y) }
|
||||||
|
@inlinable set { (x, y) = newValue[] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGSize: Animatable {
|
||||||
|
public var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||||
|
@inlinable get { .init(width, height) }
|
||||||
|
@inlinable set { (width, height) = newValue[] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGRect: Animatable {
|
||||||
|
public var animatableData: AnimatablePair<CGPoint.AnimatableData, CGSize.AnimatableData> {
|
||||||
|
@inlinable get {
|
||||||
|
.init(origin.animatableData, size.animatableData)
|
||||||
|
}
|
||||||
|
@inlinable set {
|
||||||
|
(origin.animatableData, size.animatableData) = newValue[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,9 +11,8 @@
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
#if canImport(Combine)
|
public protocol AnimatableModifier: Animatable, ViewModifier {}
|
||||||
@_exported import Combine
|
|
||||||
#else
|
|
||||||
@_exported import OpenCombine
|
|
||||||
#endif
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
// Copyright 2020 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
|
||||||
|
|
||||||
|
/// This default is specified in SwiftUI on `Animation.timingCurve` as `0.35`.
|
||||||
|
public let defaultDuration = 0.35
|
||||||
|
|
||||||
|
public struct Animation: Equatable {
|
||||||
|
fileprivate var box: _AnimationBoxBase
|
||||||
|
|
||||||
|
private init(_ box: _AnimationBoxBase) {
|
||||||
|
self.box = box
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let `default` = Self.easeInOut
|
||||||
|
|
||||||
|
public func delay(_ delay: Double) -> Animation {
|
||||||
|
.init(DelayedAnimationBox(delay: delay, parent: box))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func speed(_ speed: Double) -> Animation {
|
||||||
|
.init(RetimedAnimationBox(speed: speed, parent: box))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func repeatCount(
|
||||||
|
_ repeatCount: Int,
|
||||||
|
autoreverses: Bool = true
|
||||||
|
) -> Animation {
|
||||||
|
.init(RepeatedAnimationBox(style: .fixed(repeatCount, autoreverses: autoreverses), parent: box))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func repeatForever(autoreverses: Bool = true) -> Animation {
|
||||||
|
.init(RepeatedAnimationBox(style: .forever(autoreverses: autoreverses), parent: box))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func spring(
|
||||||
|
response: Double = 0.55,
|
||||||
|
dampingFraction: Double = 0.825,
|
||||||
|
blendDuration: Double = 0
|
||||||
|
) -> Animation {
|
||||||
|
if response == 0 { // Infinitely stiff spring
|
||||||
|
// (well, not .infinity, but a very high number)
|
||||||
|
return interpolatingSpring(stiffness: 999, damping: 999)
|
||||||
|
} else {
|
||||||
|
return interpolatingSpring(
|
||||||
|
mass: 1,
|
||||||
|
stiffness: pow(2 * .pi / response, 2),
|
||||||
|
damping: 4 * .pi * dampingFraction / response
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func interactiveSpring(
|
||||||
|
response: Double = 0.15,
|
||||||
|
dampingFraction: Double = 0.86,
|
||||||
|
blendDuration: Double = 0.25
|
||||||
|
) -> Animation {
|
||||||
|
spring(
|
||||||
|
response: response,
|
||||||
|
dampingFraction: dampingFraction,
|
||||||
|
blendDuration: blendDuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func interpolatingSpring(
|
||||||
|
mass: Double = 1.0,
|
||||||
|
stiffness: Double,
|
||||||
|
damping: Double,
|
||||||
|
initialVelocity: Double = 0.0
|
||||||
|
) -> Animation {
|
||||||
|
.init(StyleAnimationBox(style: .solver(_AnimationSolvers.Spring(
|
||||||
|
mass: mass,
|
||||||
|
stiffness: stiffness,
|
||||||
|
damping: damping,
|
||||||
|
initialVelocity: initialVelocity
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func easeInOut(duration: Double) -> Animation {
|
||||||
|
timingCurve(0.42, 0, 0.58, 1.0, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var easeInOut: Animation {
|
||||||
|
easeInOut(duration: defaultDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func easeIn(duration: Double) -> Animation {
|
||||||
|
timingCurve(0.42, 0, 1.0, 1.0, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var easeIn: Animation {
|
||||||
|
easeIn(duration: defaultDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func easeOut(duration: Double) -> Animation {
|
||||||
|
timingCurve(0, 0, 0.58, 1.0, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var easeOut: Animation {
|
||||||
|
easeOut(duration: defaultDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func linear(duration: Double) -> Animation {
|
||||||
|
timingCurve(0, 0, 1, 1, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var linear: Animation {
|
||||||
|
timingCurve(0, 0, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func timingCurve(
|
||||||
|
_ c0x: Double,
|
||||||
|
_ c0y: Double,
|
||||||
|
_ c1x: Double,
|
||||||
|
_ c1y: Double,
|
||||||
|
duration: Double = defaultDuration
|
||||||
|
) -> Animation {
|
||||||
|
.init(StyleAnimationBox(style: .timingCurve(c0x, c0y, c1x, c1y, duration: duration)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _AnimationProxy {
|
||||||
|
let subject: Animation
|
||||||
|
|
||||||
|
public init(_ subject: Animation) { self.subject = subject }
|
||||||
|
|
||||||
|
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||||
|
where Value: Equatable
|
||||||
|
{
|
||||||
|
public var animation: Animation?
|
||||||
|
public var value: Value
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(animation: Animation?, value: Value) {
|
||||||
|
self.animation = animation
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ContentWrapper: View, Equatable {
|
||||||
|
let content: Content
|
||||||
|
let animation: Animation?
|
||||||
|
let value: Value
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var lastValue: Value?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content.transaction {
|
||||||
|
if lastValue != value {
|
||||||
|
$0.animation = animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.value == rhs.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
ContentWrapper(content: content, animation: animation, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.value == rhs.value
|
||||||
|
&& lhs.animation == rhs.animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _AnimationView<Content>: View
|
||||||
|
where Content: Equatable, Content: View
|
||||||
|
{
|
||||||
|
public var content: Content
|
||||||
|
public var animation: Animation?
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(content: Content, animation: Animation?) {
|
||||||
|
self.content = content
|
||||||
|
self.animation = animation
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
content
|
||||||
|
.modifier(_AnimationModifier(animation: animation, value: content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func animation<V>(
|
||||||
|
_ animation: Animation?,
|
||||||
|
value: V
|
||||||
|
) -> some View where V: Equatable {
|
||||||
|
modifier(_AnimationModifier(animation: animation, value: value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View where Self: Equatable {
|
||||||
|
@inlinable
|
||||||
|
func animation(_ animation: Animation?) -> some View {
|
||||||
|
_AnimationView(content: self, animation: animation)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright 2020 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.
|
||||||
|
|
||||||
|
public struct Transaction {
|
||||||
|
/// The overridden transaction for a state change in a `withTransaction` block.
|
||||||
|
/// Is always set back to `nil` when the block exits.
|
||||||
|
static var _active: Self?
|
||||||
|
|
||||||
|
public var animation: Animation?
|
||||||
|
|
||||||
|
/** `true` in the first part of the transition update, this avoids situations when `animation(_:)`
|
||||||
|
could add more animations to this transaction.
|
||||||
|
*/
|
||||||
|
public var disablesAnimations: Bool
|
||||||
|
|
||||||
|
public init(animation: Animation?) {
|
||||||
|
self.animation = animation
|
||||||
|
disablesAnimations = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withTransaction<Result>(
|
||||||
|
_ transaction: Transaction,
|
||||||
|
_ body: () throws -> Result
|
||||||
|
) rethrows -> Result {
|
||||||
|
Transaction._active = transaction
|
||||||
|
defer { Transaction._active = nil }
|
||||||
|
return try body()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withAnimation<Result>(
|
||||||
|
_ animation: Animation? = .default,
|
||||||
|
_ body: () throws -> Result
|
||||||
|
) rethrows -> Result {
|
||||||
|
try withTransaction(.init(animation: animation), body)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol _TransactionModifierProtocol {
|
||||||
|
func modifyTransaction(_ transaction: inout Transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _TransactionModifier: ViewModifier {
|
||||||
|
public var transform: (inout Transaction) -> ()
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(transform: @escaping (inout Transaction) -> ()) {
|
||||||
|
self.transform = transform
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _TransactionModifier: _TransactionModifierProtocol {
|
||||||
|
func modifyTransaction(_ transaction: inout Transaction) {
|
||||||
|
transform(&transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ModifiedContent: _TransactionModifierProtocol
|
||||||
|
where Modifier: _TransactionModifierProtocol
|
||||||
|
{
|
||||||
|
func modifyTransaction(_ transaction: inout Transaction) {
|
||||||
|
modifier.modifyTransaction(&transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
|
||||||
|
public var content: V
|
||||||
|
public var base: _TransactionModifier
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
content: V,
|
||||||
|
transform: @escaping (inout Transaction) -> ()
|
||||||
|
) {
|
||||||
|
self.content = content
|
||||||
|
base = .init(transform: transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.modifier(self.content)
|
||||||
|
.modifier(base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func transaction(_ transform: @escaping (inout Transaction) -> ()) -> some View {
|
||||||
|
modifier(_TransactionModifier(transform: transform))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ViewModifier {
|
||||||
|
@inlinable
|
||||||
|
func transaction(
|
||||||
|
_ transform: @escaping (inout Transaction) -> ()
|
||||||
|
) -> some ViewModifier {
|
||||||
|
_PushPopTransactionModifier(content: self, transform: transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func animation(
|
||||||
|
_ animation: Animation?
|
||||||
|
) -> some ViewModifier {
|
||||||
|
transaction { t in
|
||||||
|
if !t.disablesAnimations {
|
||||||
|
t.animation = animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol VectorArithmetic: AdditiveArithmetic {
|
||||||
|
mutating func scale(by rhs: Double)
|
||||||
|
var magnitudeSquared: Double { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Float: VectorArithmetic {
|
||||||
|
@_transparent
|
||||||
|
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
|
@_transparent get { Double(self * self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Double: VectorArithmetic {
|
||||||
|
@_transparent
|
||||||
|
public mutating func scale(by rhs: Double) { self *= rhs }
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
|
@_transparent get { self * self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGFloat: VectorArithmetic {
|
||||||
|
@_transparent
|
||||||
|
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
|
||||||
|
|
||||||
|
@_transparent
|
||||||
|
public var magnitudeSquared: Double {
|
||||||
|
@_transparent get { Double(self * self) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class _AnimationBoxBase: Equatable {
|
||||||
|
public struct _Resolved {
|
||||||
|
public var duration: Double {
|
||||||
|
switch style {
|
||||||
|
case let .timingCurve(_, _, _, _, duration):
|
||||||
|
return duration
|
||||||
|
case let .solver(solver):
|
||||||
|
return solver.restingPoint(precision: 0.01)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var delay: Double
|
||||||
|
public var speed: Double
|
||||||
|
public var repeatStyle: _RepeatStyle
|
||||||
|
public var style: _Style
|
||||||
|
|
||||||
|
public enum _Style: Equatable {
|
||||||
|
case timingCurve(Double, Double, Double, Double, duration: Double)
|
||||||
|
case solver(_AnimationSolver)
|
||||||
|
|
||||||
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .timingCurve(lhs0, lhs1, lhs2, lhs3, lhsDuration):
|
||||||
|
if case let .timingCurve(rhs0, rhs1, rhs2, rhs3, rhsDuration) = rhs {
|
||||||
|
return lhs0 == rhs0
|
||||||
|
&& lhs1 == rhs1
|
||||||
|
&& lhs2 == rhs2
|
||||||
|
&& lhs3 == rhs3
|
||||||
|
&& lhsDuration == rhsDuration
|
||||||
|
}
|
||||||
|
case let .solver(lhsSolver):
|
||||||
|
if case let .solver(rhsSolver) = rhs {
|
||||||
|
return type(of: lhsSolver) == type(of: rhsSolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum _RepeatStyle: Equatable {
|
||||||
|
case fixed(Int, autoreverses: Bool)
|
||||||
|
case forever(autoreverses: Bool)
|
||||||
|
|
||||||
|
public var autoreverses: Bool {
|
||||||
|
switch self {
|
||||||
|
case let .fixed(_, autoreverses),
|
||||||
|
let .forever(autoreverses):
|
||||||
|
return autoreverses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve() -> _Resolved {
|
||||||
|
fatalError("implement \(#function) in subclass")
|
||||||
|
}
|
||||||
|
|
||||||
|
func equals(_ other: _AnimationBoxBase) -> Bool {
|
||||||
|
fatalError("implement \(#function) in subclass")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: _AnimationBoxBase, rhs: _AnimationBoxBase) -> Bool {
|
||||||
|
lhs.equals(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StyleAnimationBox: _AnimationBoxBase {
|
||||||
|
let style: _Resolved._Style
|
||||||
|
|
||||||
|
init(style: _Resolved._Style) {
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resolve() -> _AnimationBoxBase._Resolved {
|
||||||
|
.init(delay: 0, speed: 1, repeatStyle: .fixed(1, autoreverses: true), style: style)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func equals(_ other: _AnimationBoxBase) -> Bool {
|
||||||
|
guard let other = other as? StyleAnimationBox else { return false }
|
||||||
|
return style == other.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DelayedAnimationBox: _AnimationBoxBase {
|
||||||
|
let delay: Double
|
||||||
|
let parent: _AnimationBoxBase
|
||||||
|
|
||||||
|
init(delay: Double, parent: _AnimationBoxBase) {
|
||||||
|
self.delay = delay
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resolve() -> _AnimationBoxBase._Resolved {
|
||||||
|
var resolved = parent.resolve()
|
||||||
|
resolved.delay = delay
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
override func equals(_ other: _AnimationBoxBase) -> Bool {
|
||||||
|
guard let other = other as? DelayedAnimationBox else { return false }
|
||||||
|
return delay == other.delay && parent.equals(other.parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RetimedAnimationBox: _AnimationBoxBase {
|
||||||
|
let speed: Double
|
||||||
|
let parent: _AnimationBoxBase
|
||||||
|
|
||||||
|
init(speed: Double, parent: _AnimationBoxBase) {
|
||||||
|
self.speed = speed
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resolve() -> _AnimationBoxBase._Resolved {
|
||||||
|
var resolved = parent.resolve()
|
||||||
|
resolved.speed = speed
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
override func equals(_ other: _AnimationBoxBase) -> Bool {
|
||||||
|
guard let other = other as? RetimedAnimationBox else { return false }
|
||||||
|
return speed == other.speed && parent.equals(other.parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class RepeatedAnimationBox: _AnimationBoxBase {
|
||||||
|
let style: _AnimationBoxBase._Resolved._RepeatStyle
|
||||||
|
let parent: _AnimationBoxBase
|
||||||
|
|
||||||
|
init(style: _AnimationBoxBase._Resolved._RepeatStyle, parent: _AnimationBoxBase) {
|
||||||
|
self.style = style
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resolve() -> _AnimationBoxBase._Resolved {
|
||||||
|
var resolved = parent.resolve()
|
||||||
|
resolved.repeatStyle = style
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
override func equals(_ other: _AnimationBoxBase) -> Bool {
|
||||||
|
guard let other = other as? RepeatedAnimationBox else { return false }
|
||||||
|
return style == other.style && parent.equals(other.parent)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A solver for an animation with a duration that depends on its properties.
|
||||||
|
public protocol _AnimationSolver {
|
||||||
|
/// Solve value at a specific point in time.
|
||||||
|
func solve(at t: Double) -> Double
|
||||||
|
/// Calculates the duration of the animation to a specific precision.
|
||||||
|
func restingPoint(precision y: Double) -> Double
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum _AnimationSolvers {
|
||||||
|
// swiftlint:disable line_length
|
||||||
|
/// Calculates the animation of a spring with certain properties.
|
||||||
|
///
|
||||||
|
/// For some useful information, see
|
||||||
|
/// [Demystifying UIKit Spring Animations](https://medium.com/ios-os-x-development/demystifying-uikit-spring-animations-2bb868446773)
|
||||||
|
public struct Spring: _AnimationSolver {
|
||||||
|
// swiftlint:enable line_length
|
||||||
|
let ƛ: Double
|
||||||
|
let w0: Double
|
||||||
|
let wd: Double
|
||||||
|
/// Initial velocity
|
||||||
|
let v0: Double
|
||||||
|
/// Target value
|
||||||
|
let s0: Double = 1
|
||||||
|
|
||||||
|
public init(mass: Double, stiffness: Double, damping: Double, initialVelocity: Double) {
|
||||||
|
ƛ = (damping * 0.755) / (mass * 2)
|
||||||
|
w0 = sqrt(stiffness / 2)
|
||||||
|
wd = sqrt(abs(pow(w0, 2) - pow(ƛ, 2)))
|
||||||
|
v0 = initialVelocity
|
||||||
|
}
|
||||||
|
|
||||||
|
public func solve(at t: Double) -> Double {
|
||||||
|
let y: Double
|
||||||
|
if ƛ < w0 {
|
||||||
|
y = pow(M_E, -(ƛ * t)) * ((s0 * cos(wd * t)) + ((v0 + s0) * sin(wd * t)))
|
||||||
|
// } else if ƛ > w0 { // Overdamping is unsupported on Apple platforms
|
||||||
|
} else {
|
||||||
|
y = pow(M_E, -(ƛ * t)) * (s0 + ((v0 + (ƛ * s0)) * t))
|
||||||
|
}
|
||||||
|
return 1 - y
|
||||||
|
}
|
||||||
|
|
||||||
|
public func restingPoint(precision y: Double) -> Double {
|
||||||
|
log(y) / -ƛ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ?? ()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol _VectorMath: Animatable {}
|
||||||
|
|
||||||
|
public extension _VectorMath {
|
||||||
|
@inlinable
|
||||||
|
var magnitude: Double {
|
||||||
|
animatableData.magnitudeSquared.squareRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
mutating func negate() {
|
||||||
|
animatableData = .zero - animatableData
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static prefix func - (operand: Self) -> Self {
|
||||||
|
var result = operand
|
||||||
|
result.negate()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func += (lhs: inout Self, rhs: Self) {
|
||||||
|
lhs.animatableData += rhs.animatableData
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
|
var result = lhs
|
||||||
|
result += rhs
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func -= (lhs: inout Self, rhs: Self) {
|
||||||
|
lhs.animatableData -= rhs.animatableData
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func - (lhs: Self, rhs: Self) -> Self {
|
||||||
|
var result = lhs
|
||||||
|
result -= rhs
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func *= (lhs: inout Self, rhs: Double) {
|
||||||
|
lhs.animatableData.scale(by: rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func * (lhs: Self, rhs: Double) -> Self {
|
||||||
|
var result = lhs
|
||||||
|
result *= rhs
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func /= (lhs: inout Self, rhs: Double) {
|
||||||
|
lhs *= 1 / rhs
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func / (lhs: Self, rhs: Double) -> Self {
|
||||||
|
var result = lhs
|
||||||
|
result /= rhs
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/16/20.
|
// Created by Carson Katri on 7/16/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
/// Provides the ability to set the title of the Scene.
|
/// Provides the ability to set the title of the Scene.
|
||||||
public protocol _TitledApp {
|
public protocol _TitledApp {
|
||||||
|
@ -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 extension App {
|
public struct _AppConfiguration {
|
||||||
static func main() {
|
public let reconciler: Reconciler
|
||||||
let app = Self()
|
public let rootEnvironment: EnvironmentValues
|
||||||
_launch(app, EnvironmentValues())
|
|
||||||
|
public init(
|
||||||
|
reconciler: Reconciler = .stack,
|
||||||
|
rootEnvironment: EnvironmentValues = .init()
|
||||||
|
) {
|
||||||
|
self.reconciler = reconciler
|
||||||
|
self.rootEnvironment = rootEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Reconciler {
|
||||||
|
/// Use the `StackReconciler`.
|
||||||
|
case stack
|
||||||
|
/// Use the `FiberReconciler` with layout steps optionally enabled.
|
||||||
|
case fiber(useDynamicLayout: Bool = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension App {
|
||||||
|
static var _configuration: _AppConfiguration { .init() }
|
||||||
|
|
||||||
|
static func main() {
|
||||||
|
let app = Self()
|
||||||
|
_launch(app, with: Self._configuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,11 +15,15 @@
|
||||||
// Created by Carson Katri on 7/16/20.
|
// Created by Carson Katri on 7/16/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
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!
|
||||||
}
|
}
|
||||||
|
@ -173,6 +177,7 @@ struct DefaultAppStorageEnvironmentKey: EnvironmentKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension EnvironmentValues {
|
public extension EnvironmentValues {
|
||||||
|
@_spi(TokamakCore)
|
||||||
var _defaultAppStorage: _StorageProvider? {
|
var _defaultAppStorage: _StorageProvider? {
|
||||||
get {
|
get {
|
||||||
self[DefaultAppStorageEnvironmentKey.self]
|
self[DefaultAppStorageEnvironmentKey.self]
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/16/20.
|
// Created by Carson Katri on 7/16/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
@_functionBuilder
|
@resultBuilder
|
||||||
public enum SceneBuilder {
|
public enum SceneBuilder {
|
||||||
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
|
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
|
||||||
content
|
content
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/17/20.
|
// Created by Carson Katri on 7/17/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
/// The renderer must specify a default `_StorageProvider` before any `SceneStorage`
|
/// The renderer must specify a default `_StorageProvider` before any `SceneStorage`
|
||||||
/// values are accessed.
|
/// values are accessed.
|
||||||
|
@ -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) -> ()
|
||||||
|
|
|
@ -63,6 +63,7 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public var body: Never {
|
public var body: Never {
|
||||||
neverScene("WindowGroup")
|
neverScene("WindowGroup")
|
||||||
}
|
}
|
||||||
|
@ -74,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ public struct _SceneModifier_Content<Modifier>: Scene where Modifier: _SceneModi
|
||||||
public let modifier: Modifier
|
public let modifier: Modifier
|
||||||
public let scene: _AnyScene
|
public let scene: _AnyScene
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public var body: Never {
|
public var body: Never {
|
||||||
neverScene("_SceneModifier_Content")
|
neverScene("_SceneModifier_Content")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/19/20.
|
// Created by Carson Katri on 7/19/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
public struct _AnyApp: App {
|
public struct _AnyApp: App {
|
||||||
var app: Any
|
var app: Any
|
||||||
|
@ -31,26 +31,36 @@ public struct _AnyApp: App {
|
||||||
bodyType = A.Body.self
|
bodyType = A.Body.self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public var body: Never {
|
public var body: Never {
|
||||||
neverScene("_AnyApp")
|
neverScene("_AnyApp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public init() {
|
public init() {
|
||||||
fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.")
|
fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
|
@_spi(TokamakCore)
|
||||||
|
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public static func _setTitle(_ title: String) {
|
public static func _setTitle(_ title: String) {
|
||||||
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)
|
||||||
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||||
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
|
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ public struct _AnyScene: Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
public var body: Never {
|
public var body: Never {
|
||||||
neverScene("_AnyScene")
|
neverScene("_AnyScene")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/22/20.
|
// Created by Carson Katri on 7/22/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
public protocol _StorageProvider {
|
public protocol _StorageProvider {
|
||||||
func store(key: String, value: Bool?)
|
func store(key: String, value: Bool?)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,12 +15,14 @@
|
||||||
// Created by Carson Katri on 7/7/20.
|
// Created by Carson Katri on 7/7/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
public struct EnvironmentValues: CustomStringConvertible {
|
public struct EnvironmentValues: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
|
@ -43,6 +43,22 @@ public struct EnvironmentValues: CustomStringConvertible {
|
||||||
values[bindable] = newValue
|
values[bindable] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public mutating func merge(_ other: Self?) {
|
||||||
|
if let other = other {
|
||||||
|
values.merge(other.values) { _, new in
|
||||||
|
new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public func merging(_ other: Self?) -> Self {
|
||||||
|
var merged = self
|
||||||
|
merged.merge(other)
|
||||||
|
return merged
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IsEnabledKey: EnvironmentKey {
|
struct IsEnabledKey: EnvironmentKey {
|
||||||
|
@ -60,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,46 @@
|
||||||
|
// Copyright 2020 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 7/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// A modifier that resolves to a concrete modifier in an environment.
|
||||||
|
public protocol EnvironmentalModifier: ViewModifier {
|
||||||
|
associatedtype ResolvedModifier: ViewModifier
|
||||||
|
func resolve(in environment: EnvironmentValues) -> ResolvedModifier
|
||||||
|
static var _requiresMainThread: Bool { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct EnvironmentalModifierResolver<M>: ViewModifier, EnvironmentReader
|
||||||
|
where M: EnvironmentalModifier
|
||||||
|
{
|
||||||
|
let modifier: M
|
||||||
|
var resolved: M.ResolvedModifier!
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content.modifier(resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func setContent(from values: EnvironmentValues) {
|
||||||
|
resolved = modifier.resolve(in: values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension EnvironmentalModifier {
|
||||||
|
static var _requiresMainThread: Bool { true }
|
||||||
|
|
||||||
|
func body(content: _ViewModifier_Content<Self>) -> some View {
|
||||||
|
content.modifier(EnvironmentalModifierResolver(modifier: self))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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!
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2020-2021 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
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public enum ContentMode: Hashable, CaseIterable {
|
||||||
|
case fit
|
||||||
|
case fill
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _AspectRatioLayout: ViewModifier {
|
||||||
|
public let aspectRatio: CGFloat?
|
||||||
|
public let contentMode: ContentMode
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(aspectRatio: CGFloat?, contentMode: ContentMode) {
|
||||||
|
self.aspectRatio = aspectRatio
|
||||||
|
self.contentMode = contentMode
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func aspectRatio(
|
||||||
|
_ aspectRatio: CGFloat? = nil,
|
||||||
|
contentMode: ContentMode
|
||||||
|
) -> some View {
|
||||||
|
modifier(
|
||||||
|
_AspectRatioLayout(
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
contentMode: contentMode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func aspectRatio(
|
||||||
|
_ aspectRatio: CGSize,
|
||||||
|
contentMode: ContentMode
|
||||||
|
) -> some View {
|
||||||
|
self.aspectRatio(
|
||||||
|
aspectRatio.width / aspectRatio.height,
|
||||||
|
contentMode: contentMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func scaledToFit() -> some View {
|
||||||
|
aspectRatio(contentMode: .fit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func scaledToFill() -> some View {
|
||||||
|
aspectRatio(contentMode: .fill)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,6 +15,8 @@
|
||||||
// Created by Carson Katri on 06/29/2020.
|
// Created by Carson Katri on 06/29/2020.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
|
public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
|
||||||
public var shape: ClipShape
|
public var shape: ClipShape
|
||||||
public var style: FillStyle
|
public var style: FillStyle
|
||||||
|
@ -27,6 +29,11 @@ public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
|
||||||
public func body(content: Content) -> some View {
|
public func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var animatableData: ClipShape.AnimatableData {
|
||||||
|
get { shape.animatableData }
|
||||||
|
set { shape.animatableData = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,8 +15,10 @@
|
||||||
// Created by Carson Katri on 7/3/20.
|
// Created by Carson Katri on 7/3/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
// FIXME: Make `Animatable`
|
// FIXME: Make `Animatable`
|
||||||
public protocol GeometryEffect: ViewModifier {
|
public protocol GeometryEffect: Animatable, ViewModifier {
|
||||||
func effectValue(size: CGSize) -> ProjectionTransform
|
func effectValue(size: CGSize) -> ProjectionTransform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2020-2021 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 7/12/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _OffsetEffect: GeometryEffect, Equatable {
|
||||||
|
public var offset: CGSize
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(offset: CGSize) {
|
||||||
|
self.offset = offset
|
||||||
|
}
|
||||||
|
|
||||||
|
public func effectValue(size: CGSize) -> ProjectionTransform {
|
||||||
|
.init(.init(translationX: offset.width, y: offset.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var animatableData: CGSize.AnimatableData {
|
||||||
|
get {
|
||||||
|
offset.animatableData
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
offset.animatableData = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func offset(_ offset: CGSize) -> some View {
|
||||||
|
modifier(_OffsetEffect(offset: offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func offset(x: CGFloat = 0, y: CGFloat = 0) -> some View {
|
||||||
|
offset(CGSize(width: x, height: y))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2021 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 1/20/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
public struct _OpacityEffect: Animatable, ViewModifier, Equatable {
|
||||||
|
public var opacity: Double
|
||||||
|
|
||||||
|
public init(opacity: Double) {
|
||||||
|
self.opacity = opacity
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
public var animatableData: Double {
|
||||||
|
get { opacity }
|
||||||
|
set { opacity = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func opacity(_ opacity: Double) -> some View {
|
||||||
|
modifier(_OpacityEffect(opacity: opacity))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,6 +15,8 @@
|
||||||
// Created by Carson Katri on 7/3/20.
|
// Created by Carson Katri on 7/3/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct _RotationEffect: GeometryEffect {
|
public struct _RotationEffect: GeometryEffect {
|
||||||
public var angle: Angle
|
public var angle: Angle
|
||||||
public var anchor: UnitPoint
|
public var anchor: UnitPoint
|
||||||
|
@ -25,12 +27,21 @@ public struct _RotationEffect: GeometryEffect {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func effectValue(size: CGSize) -> ProjectionTransform {
|
public func effectValue(size: CGSize) -> ProjectionTransform {
|
||||||
.init(CGAffineTransform.identity.rotated(by: angle.radians))
|
.init(CGAffineTransform.identity.rotated(by: CGFloat(angle.radians)))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
public func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var animatableData: AnimatablePair<Angle.AnimatableData, UnitPoint.AnimatableData> {
|
||||||
|
get {
|
||||||
|
.init(angle.animatableData, anchor.animatableData)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
(angle.animatableData, anchor.animatableData) = newValue[]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2020-2021 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 7/9/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||||
|
public var scale: CGSize
|
||||||
|
public var anchor: UnitPoint
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(scale: CGSize, anchor: UnitPoint = .center) {
|
||||||
|
self.scale = scale
|
||||||
|
self.anchor = anchor
|
||||||
|
}
|
||||||
|
|
||||||
|
public func effectValue(size: CGSize) -> ProjectionTransform {
|
||||||
|
.init(.init(scaleX: scale.width, y: scale.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View {
|
||||||
|
modifier(_ScaleEffect(scale: scale, anchor: anchor))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some View {
|
||||||
|
scaleEffect(CGSize(width: s, height: s), anchor: anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func scaleEffect(
|
||||||
|
x: CGFloat = 1.0,
|
||||||
|
y: CGFloat = 1.0,
|
||||||
|
anchor: UnitPoint = .center
|
||||||
|
) -> some View {
|
||||||
|
scaleEffect(CGSize(width: x, height: y), anchor: anchor)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,6 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct _FlexFrameLayout: ViewModifier {
|
public struct _FlexFrameLayout: ViewModifier {
|
||||||
public let minWidth: CGFloat?
|
public let minWidth: CGFloat?
|
||||||
public let idealWidth: CGFloat?
|
public let idealWidth: CGFloat?
|
||||||
|
@ -24,11 +26,11 @@ public struct _FlexFrameLayout: ViewModifier {
|
||||||
// These are special cases in SwiftUI, where the child
|
// These are special cases in SwiftUI, where the child
|
||||||
// will request the entire width/height of the parent.
|
// will request the entire width/height of the parent.
|
||||||
public var fillWidth: Bool {
|
public var fillWidth: Bool {
|
||||||
minWidth == 0 && maxWidth == .infinity
|
(minWidth == 0 || minWidth == nil) && maxWidth == .infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
public var fillHeight: Bool {
|
public var fillHeight: Bool {
|
||||||
minHeight == 0 && maxHeight == .infinity
|
(minHeight == 0 || minHeight == nil) && maxHeight == .infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
@ -54,6 +56,10 @@ public struct _FlexFrameLayout: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _FlexFrameLayout: Animatable {
|
||||||
|
public typealias AnimatableData = EmptyAnimatableData
|
||||||
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func frame(
|
func frame(
|
||||||
minWidth: CGFloat? = nil,
|
minWidth: CGFloat? = nil,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,6 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct _FrameLayout: ViewModifier {
|
public struct _FrameLayout: ViewModifier {
|
||||||
public let width: CGFloat?
|
public let width: CGFloat?
|
||||||
public let height: CGFloat?
|
public let height: CGFloat?
|
||||||
|
@ -28,6 +30,10 @@ public struct _FrameLayout: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _FrameLayout: Animatable {
|
||||||
|
public typealias AnimatableData = EmptyAnimatableData
|
||||||
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func frame(
|
func frame(
|
||||||
width: CGFloat? = nil,
|
width: CGFloat? = nil,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2021 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.
|
||||||
|
/// Underscore is present in the name for SwiftUI compatibility.
|
||||||
|
public struct _HoverActionModifier: ViewModifier {
|
||||||
|
public var hover: ((Bool) -> ())?
|
||||||
|
|
||||||
|
public typealias Body = Never
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ModifiedContent
|
||||||
|
where Content: View, Modifier == _HoverActionModifier
|
||||||
|
{
|
||||||
|
var hover: ((Bool) -> ())? { modifier.hover }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func onHover(perform action: ((Bool) -> ())?) -> some View {
|
||||||
|
modifier(_HoverActionModifier(hover: action))
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,34 @@
|
||||||
|
|
||||||
// FIXME: these should have standalone implementations
|
// FIXME: these should have standalone implementations
|
||||||
public extension View {
|
public extension View {
|
||||||
|
@_spi(TokamakCore)
|
||||||
func _onMount(perform action: (() -> ())? = nil) -> some View {
|
func _onMount(perform action: (() -> ())? = nil) -> some View {
|
||||||
modifier(_AppearanceActionModifier(appear: action))
|
modifier(_AppearanceActionModifier(appear: action))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
func _onUpdate(perform action: (() -> ())? = nil) -> some View {
|
||||||
|
modifier(_LifecycleActionModifier(update: action))
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
func _onUnmount(perform action: (() -> ())? = nil) -> some View {
|
func _onUnmount(perform action: (() -> ())? = nil) -> some View {
|
||||||
modifier(_AppearanceActionModifier(disappear: action))
|
modifier(_AppearanceActionModifier(disappear: action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol LifecycleActionType {
|
||||||
|
var update: (() -> ())? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct _LifecycleActionModifier: ViewModifier {
|
||||||
|
var update: (() -> ())?
|
||||||
|
|
||||||
|
typealias Body = Never
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ModifiedContent: LifecycleActionType
|
||||||
|
where Content: View, Modifier == _LifecycleActionModifier
|
||||||
|
{
|
||||||
|
var update: (() -> ())? { modifier.update }
|
||||||
|
}
|
||||||
|
|
|
@ -13,12 +13,16 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
protocol ModifierContainer {
|
protocol ModifierContainer {
|
||||||
var environmentModifier: EnvironmentModifier? { get }
|
var environmentModifier: _EnvironmentModifier? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol ModifiedContentProtocol {}
|
||||||
|
|
||||||
/// A value with a modifier applied to it.
|
/// A value with a modifier applied to it.
|
||||||
public struct ModifiedContent<Content, Modifier> {
|
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
|
||||||
|
@ -30,7 +34,7 @@ public struct ModifiedContent<Content, Modifier> {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -39,7 +43,7 @@ extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier {
|
extension ModifiedContent: View, GroupView, ParentView where Content: View, Modifier: ViewModifier {
|
||||||
public var body: Body {
|
public var body: Body {
|
||||||
neverBody("ModifiedContent<View, ViewModifier>")
|
neverBody("ModifiedContent<View, ViewModifier>")
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,32 @@ public extension View {
|
||||||
navigationTitle(title)
|
navigationTitle(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(
|
||||||
|
*,
|
||||||
|
deprecated,
|
||||||
|
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
|
||||||
|
)
|
||||||
|
func navigationBarTitle(
|
||||||
|
_ title: Text,
|
||||||
|
displayMode: NavigationBarItem.TitleDisplayMode
|
||||||
|
) -> some View {
|
||||||
|
navigationTitle(title)
|
||||||
|
.navigationBarTitleDisplayMode(displayMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(
|
||||||
|
*,
|
||||||
|
deprecated,
|
||||||
|
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
|
||||||
|
)
|
||||||
|
func navigationBarTitle<S: StringProtocol>(
|
||||||
|
_ title: S,
|
||||||
|
displayMode: NavigationBarItem.TitleDisplayMode
|
||||||
|
) -> some View {
|
||||||
|
navigationTitle(title)
|
||||||
|
.navigationBarTitleDisplayMode(displayMode)
|
||||||
|
}
|
||||||
|
|
||||||
func navigationTitle(_ title: Text) -> some View {
|
func navigationTitle(_ title: Text) -> some View {
|
||||||
navigationTitle { title }
|
navigationTitle { title }
|
||||||
}
|
}
|
||||||
|
@ -36,4 +62,11 @@ public extension View {
|
||||||
{
|
{
|
||||||
preference(key: NavigationTitleKey.self, value: AnyView(title()))
|
preference(key: NavigationTitleKey.self, value: AnyView(title()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func navigationBarTitleDisplayMode(
|
||||||
|
_ displayMode: NavigationBarItem
|
||||||
|
.TitleDisplayMode
|
||||||
|
) -> some View {
|
||||||
|
preference(key: NavigationBarItemKey.self, value: .init(displayMode: displayMode))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -12,6 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public struct _PaddingLayout: ViewModifier {
|
public struct _PaddingLayout: ViewModifier {
|
||||||
public var edges: Edge.Set
|
public var edges: Edge.Set
|
||||||
public var insets: EdgeInsets?
|
public var insets: EdgeInsets?
|
||||||
|
@ -26,17 +28,36 @@ public struct _PaddingLayout: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _PaddingLayout: Animatable {
|
||||||
|
public typealias AnimatableData = EmptyAnimatableData
|
||||||
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
func padding(_ insets: EdgeInsets) -> some View {
|
func padding(_ insets: EdgeInsets) -> ModifiedContent<Self, _PaddingLayout> {
|
||||||
modifier(_PaddingLayout(insets: insets))
|
modifier(_PaddingLayout(insets: insets))
|
||||||
}
|
}
|
||||||
|
|
||||||
func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
|
func padding(
|
||||||
|
_ edges: Edge.Set = .all,
|
||||||
|
_ length: CGFloat? = nil
|
||||||
|
) -> ModifiedContent<Self, _PaddingLayout> {
|
||||||
let insets = length.map { EdgeInsets(_all: $0) }
|
let insets = length.map { EdgeInsets(_all: $0) }
|
||||||
return modifier(_PaddingLayout(edges: edges, insets: insets))
|
return modifier(_PaddingLayout(edges: edges, insets: insets))
|
||||||
}
|
}
|
||||||
|
|
||||||
func padding(_ length: CGFloat) -> some View {
|
func padding(_ length: CGFloat) -> ModifiedContent<Self, _PaddingLayout> {
|
||||||
padding(.all, length)
|
padding(.all, length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension ModifiedContent where Modifier == _PaddingLayout, Content: View {
|
||||||
|
func padding(_ length: CGFloat) -> ModifiedContent<Content, _PaddingLayout> {
|
||||||
|
var layout = modifier
|
||||||
|
layout.insets?.top += length
|
||||||
|
layout.insets?.leading += length
|
||||||
|
layout.insets?.bottom += length
|
||||||
|
layout.insets?.trailing += length
|
||||||
|
|
||||||
|
return ModifiedContent(content: content, modifier: layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,26 +1,106 @@
|
||||||
public struct _ShadowLayout: ViewModifier, EnvironmentReader {
|
// Copyright 2021 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
|
||||||
|
|
||||||
|
public struct _ShadowEffect: EnvironmentalModifier, Equatable {
|
||||||
public var color: Color
|
public var color: Color
|
||||||
public var radius: CGFloat
|
public var radius: CGFloat
|
||||||
public var x: CGFloat
|
public var offset: CGSize
|
||||||
public var y: CGFloat
|
|
||||||
public var environment: EnvironmentValues!
|
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
@inlinable
|
||||||
content
|
init(
|
||||||
|
color: Color,
|
||||||
|
radius: CGFloat,
|
||||||
|
offset: CGSize
|
||||||
|
) {
|
||||||
|
self.color = color
|
||||||
|
self.radius = radius
|
||||||
|
self.offset = offset
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func setContent(from values: EnvironmentValues) {
|
public func resolve(in environment: EnvironmentValues) -> _Resolved {
|
||||||
environment = values
|
.init(
|
||||||
|
color: color.provider.resolve(in: environment),
|
||||||
|
radius: radius,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _Resolved: ViewModifier, Animatable {
|
||||||
|
public var color: AnyColorBox.ResolvedValue
|
||||||
|
public var radius: CGFloat
|
||||||
|
public var offset: CGSize
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias AnimatableData = AnimatablePair<
|
||||||
|
AnimatablePair<
|
||||||
|
Float,
|
||||||
|
AnimatablePair<
|
||||||
|
Float,
|
||||||
|
AnimatablePair<Float, Float>
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
AnimatablePair<CGFloat, CGSize.AnimatableData>
|
||||||
|
>
|
||||||
|
public var animatableData: _Resolved.AnimatableData {
|
||||||
|
get {
|
||||||
|
.init(
|
||||||
|
.init(
|
||||||
|
Float(color.red),
|
||||||
|
.init(
|
||||||
|
Float(color.green),
|
||||||
|
.init(
|
||||||
|
Float(color.blue),
|
||||||
|
Float(color.opacity)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
.init(radius, offset.animatableData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
color = .init(
|
||||||
|
red: Double(newValue[].0[].0),
|
||||||
|
green: Double(newValue[].0[].1[].0),
|
||||||
|
blue: Double(newValue[].0[].1[].1[].0),
|
||||||
|
opacity: Double(newValue[].0[].1[].1[].1),
|
||||||
|
space: .sRGB
|
||||||
|
)
|
||||||
|
(radius, offset.animatableData) = newValue[].1[]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
@inlinable
|
||||||
func shadow(
|
func shadow(
|
||||||
color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
|
color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
|
||||||
radius: CGFloat,
|
radius: CGFloat,
|
||||||
x: CGFloat = 0,
|
x: CGFloat = 0,
|
||||||
y: CGFloat = 0
|
y: CGFloat = 0
|
||||||
) -> some View {
|
) -> some View {
|
||||||
modifier(_ShadowLayout(color: color, radius: radius, x: x, y: y))
|
modifier(
|
||||||
|
_ShadowEffect(
|
||||||
|
color: color,
|
||||||
|
radius: radius,
|
||||||
|
offset: .init(width: x, height: y)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Tokamak contributors
|
// Copyright 2020-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,6 +15,29 @@
|
||||||
// Created by Carson Katri on 6/29/20.
|
// Created by Carson Katri on 6/29/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Override this View's body to provide a layout that fits the background to the content.
|
||||||
|
public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
||||||
|
where Content: View, Background: View
|
||||||
|
{
|
||||||
|
public let content: Content
|
||||||
|
public let background: Background
|
||||||
|
public let alignment: Alignment
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public init(content: Content, background: Background, alignment: Alignment) {
|
||||||
|
self.content = content
|
||||||
|
self.background = background
|
||||||
|
self.alignment = alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitor.visit(background)
|
||||||
|
visitor.visit(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||||
where Background: View
|
where Background: View
|
||||||
{
|
{
|
||||||
|
@ -28,11 +51,11 @@ public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
public func body(content: Content) -> some View {
|
||||||
// FIXME: Clip to bounds of foreground.
|
_BackgroundLayout(
|
||||||
ZStack(alignment: alignment) {
|
content: content,
|
||||||
background
|
background: background,
|
||||||
content
|
alignment: alignment
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func setContent(from values: EnvironmentValues) {
|
mutating func setContent(from values: EnvironmentValues) {
|
||||||
|
@ -56,6 +79,74 @@ public extension View {
|
||||||
) -> some View where Background: View {
|
) -> some View where Background: View {
|
||||||
modifier(_BackgroundModifier(background: background, alignment: alignment))
|
modifier(_BackgroundModifier(background: background, alignment: alignment))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func background<V>(
|
||||||
|
alignment: Alignment = .center,
|
||||||
|
@ViewBuilder content: () -> V
|
||||||
|
) -> some View where V: View {
|
||||||
|
background(content(), alignment: alignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||||
|
where Style: ShapeStyle, Bounds: Shape
|
||||||
|
{
|
||||||
|
public var environment: EnvironmentValues!
|
||||||
|
|
||||||
|
public var style: Style
|
||||||
|
public var shape: Bounds
|
||||||
|
public var fillStyle: FillStyle
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(style: Style, shape: Bounds, fillStyle: FillStyle) {
|
||||||
|
self.style = style
|
||||||
|
self.shape = shape
|
||||||
|
self.fillStyle = fillStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.background(shape.fill(style, style: fillStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func setContent(from values: EnvironmentValues) {
|
||||||
|
environment = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@inlinable
|
||||||
|
func background<S, T>(
|
||||||
|
_ style: S,
|
||||||
|
in shape: T,
|
||||||
|
fillStyle: FillStyle = FillStyle()
|
||||||
|
) -> some View where S: ShapeStyle, T: Shape {
|
||||||
|
modifier(_BackgroundShapeModifier(style: style, shape: shape, fillStyle: fillStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func background<S>(
|
||||||
|
in shape: S,
|
||||||
|
fillStyle: FillStyle = FillStyle()
|
||||||
|
) -> some View where S: Shape {
|
||||||
|
background(BackgroundStyle(), in: shape, fillStyle: fillStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override this View's body to provide a layout that fits the background to the content.
|
||||||
|
public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
||||||
|
where Content: View, Overlay: View
|
||||||
|
{
|
||||||
|
public let content: Content
|
||||||
|
public let overlay: Overlay
|
||||||
|
public let alignment: Alignment
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitor.visit(content)
|
||||||
|
visitor.visit(overlay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||||
|
@ -71,11 +162,11 @@ public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
public func body(content: Content) -> some View {
|
||||||
// FIXME: Clip to content shape.
|
_OverlayLayout(
|
||||||
ZStack(alignment: alignment) {
|
content: content,
|
||||||
content
|
overlay: overlay,
|
||||||
overlay
|
alignment: alignment
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func setContent(from values: EnvironmentValues) {
|
mutating func setContent(from values: EnvironmentValues) {
|
||||||
|
@ -96,6 +187,21 @@ public extension View {
|
||||||
modifier(_OverlayModifier(overlay: overlay, alignment: alignment))
|
modifier(_OverlayModifier(overlay: overlay, alignment: alignment))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func overlay<V>(
|
||||||
|
alignment: Alignment = .center,
|
||||||
|
@ViewBuilder content: () -> V
|
||||||
|
) -> some View where V: View {
|
||||||
|
modifier(_OverlayModifier(overlay: content(), alignment: alignment))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func overlay<S>(
|
||||||
|
_ style: S
|
||||||
|
) -> some View where S: ShapeStyle {
|
||||||
|
overlay(Rectangle().fill(style))
|
||||||
|
}
|
||||||
|
|
||||||
func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle {
|
func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle {
|
||||||
overlay(Rectangle().strokeBorder(content, lineWidth: width))
|
overlay(Rectangle().strokeBorder(content, lineWidth: width))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func task(
|
||||||
|
priority: TaskPriority = .userInitiated,
|
||||||
|
_ action: @escaping @Sendable () async -> ()
|
||||||
|
) -> some View {
|
||||||
|
var task: Task<(), Never>?
|
||||||
|
return onAppear {
|
||||||
|
task = Task(priority: priority, operation: action)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
task?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,20 +16,51 @@ 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 struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier {
|
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
|
||||||
|
where Modifier: ViewModifier
|
||||||
|
{
|
||||||
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 var body: AnyView {
|
public init<V: View>(modifier: Modifier, view: V) {
|
||||||
|
self.modifier = modifier
|
||||||
|
self.view = AnyView(view)
|
||||||
|
visitChildren = { $0.visit(view) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||||
|
visitChildren(visitor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -15,33 +15,51 @@
|
||||||
// Created by Carson Katri on 7/19/20.
|
// Created by Carson Katri on 7/19/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
// This is very similar to `MountedCompositeView`. However, the `mountedBody`
|
// This is very similar to `MountedCompositeView`. However, the `mountedBody`
|
||||||
// is the computed content of the specified `Scene`, instead of having child
|
// is the computed content of the specified `Scene`, instead of having child
|
||||||
// `View`s
|
// `View`s
|
||||||
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
||||||
override func mount(
|
override func mount(
|
||||||
before _: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on _: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction
|
||||||
) {
|
) {
|
||||||
|
super.prepareForMount(with: transaction)
|
||||||
|
|
||||||
// `App` elements have no siblings, hence the `before` argument is discarded.
|
// `App` elements have no siblings, hence the `before` argument is discarded.
|
||||||
// They also have no parents, so the `parent` argument is discarded as well.
|
// They also have no parents, so the `parent` argument is discarded as well.
|
||||||
let childBody = reconciler.render(mountedApp: self)
|
let childBody = reconciler.render(mountedApp: self)
|
||||||
|
|
||||||
let child: MountedElement<R> = mountChild(childBody)
|
let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
|
||||||
mountedChildren = [child]
|
mountedChildren = [child]
|
||||||
child.mount(before: nil, on: self, with: reconciler)
|
child.transaction = transaction
|
||||||
|
child.mount(before: nil, on: self, in: reconciler, with: transaction)
|
||||||
|
|
||||||
|
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
override func unmount(
|
||||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
mountedChildren
|
||||||
|
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> {
|
/// Mounts a child scene within the app.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - renderer: An instance conforming to the `Renderer` protocol to render the mounted
|
||||||
|
/// scene with.
|
||||||
|
/// - childBody: The body of the child scene to mount for this app.
|
||||||
|
/// - Returns: Returns an instance of the `MountedScene` class that's already mounted in this app.
|
||||||
|
private func mountChild(_ renderer: R, _ childBody: _AnyScene) -> MountedScene<R> {
|
||||||
let mountedScene: MountedScene<R> = childBody
|
let mountedScene: MountedScene<R> = childBody
|
||||||
.makeMountedScene(parentTarget, environmentValues, self)
|
.makeMountedScene(renderer, parentTarget, environmentValues, self)
|
||||||
if let title = mountedScene.title {
|
if let title = mountedScene.title {
|
||||||
// swiftlint:disable force_cast
|
// swiftlint:disable force_cast
|
||||||
(app.type as! _TitledApp.Type)._setTitle(title)
|
(app.type as! _TitledApp.Type)._setTitle(title)
|
||||||
|
@ -49,17 +67,19 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
||||||
return mountedScene
|
return mountedScene
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(with reconciler: StackReconciler<R>) {
|
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||||
let element = reconciler.render(mountedApp: self)
|
let element = reconciler.render(mountedApp: self)
|
||||||
reconciler.reconcile(
|
reconciler.reconcile(
|
||||||
self,
|
self,
|
||||||
with: element,
|
with: element,
|
||||||
|
transaction: transaction,
|
||||||
getElementType: { $0.type },
|
getElementType: { $0.type },
|
||||||
updateChild: {
|
updateChild: {
|
||||||
$0.environmentValues = environmentValues
|
$0.environmentValues = environmentValues
|
||||||
$0.scene = _AnyScene(element)
|
$0.scene = _AnyScene(element)
|
||||||
|
$0.transaction = transaction
|
||||||
},
|
},
|
||||||
mountChild: { mountChild($0) }
|
mountChild: { mountChild(reconciler.renderer, $0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2018-2020 Tokamak contributors
|
// Copyright 2018-2021 Tokamak contributors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
// Created by Carson Katri on 7/19/20.
|
// Created by Carson Katri on 7/19/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
||||||
let parentTarget: R.TargetType
|
let parentTarget: R.TargetType
|
||||||
|
@ -61,10 +61,11 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
||||||
_ view: AnyView,
|
_ view: AnyView,
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues,
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ viewTraits: _ViewTraitStore,
|
||||||
_ parent: MountedElement<R>?
|
_ parent: MountedElement<R>?
|
||||||
) {
|
) {
|
||||||
self.parentTarget = parentTarget
|
self.parentTarget = parentTarget
|
||||||
super.init(view, environmentValues, parent)
|
super.init(view, environmentValues, viewTraits, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,25 +15,43 @@
|
||||||
// Created by Max Desiatov on 03/12/2018.
|
// Created by Max Desiatov on 03/12/2018.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
override func mount(
|
override func mount(
|
||||||
before sibling: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on parent: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction
|
||||||
) {
|
) {
|
||||||
|
super.prepareForMount(with: transaction)
|
||||||
|
|
||||||
|
var transaction = transaction
|
||||||
|
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||||
|
// Disable animations on mount so `animation(_:)` doesn't try to animate
|
||||||
|
// until the transition finishes.
|
||||||
|
transaction.disablesAnimations = true
|
||||||
|
self.transaction = transaction
|
||||||
|
|
||||||
|
updateVariadicView()
|
||||||
|
|
||||||
let childBody = reconciler.render(compositeView: self)
|
let childBody = reconciler.render(compositeView: self)
|
||||||
|
|
||||||
|
if let traitModifier = view.view as? _TraitWritingModifierProtocol {
|
||||||
|
traitModifier.modifyViewTraitStore(&viewTraits)
|
||||||
|
}
|
||||||
let child: MountedElement<R> = childBody.makeMountedView(
|
let child: MountedElement<R> = childBody.makeMountedView(
|
||||||
|
reconciler.renderer,
|
||||||
parentTarget,
|
parentTarget,
|
||||||
environmentValues,
|
environmentValues,
|
||||||
|
viewTraits,
|
||||||
self
|
self
|
||||||
)
|
)
|
||||||
mountedChildren = [child]
|
mountedChildren = [child]
|
||||||
child.mount(before: sibling, on: self, with: reconciler)
|
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||||
|
|
||||||
// `_TargetRef` is a composite view, so it's enough to check for it only here
|
// `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite
|
||||||
|
// view, so it's enough check for it only here.
|
||||||
if var targetRef = view.view as? TargetRefType {
|
if var targetRef = view.view as? TargetRefType {
|
||||||
// `_TargetRef` body is not always a host view that has a target, need to traverse
|
// `_TargetRef` body is not always a host view that has a target, need to traverse
|
||||||
// all descendants to find a `MountedHostView<R>` instance.
|
// all descendants to find a `MountedHostView<R>` instance.
|
||||||
|
@ -51,7 +69,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
reconciler.afterCurrentRender(perform: { [weak self] in
|
reconciler.afterCurrentRender(perform: { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
|
// FIXME: this has to be implemented in a renderer-specific way, otherwise it's equivalent to
|
||||||
// `_onMount` and `_onUnmount` at the moment,
|
// `_onMount` and `_onUnmount` at the moment,
|
||||||
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
|
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
|
||||||
if let appearanceAction = self.view.view as? AppearanceActionType {
|
if let appearanceAction = self.view.view as? AppearanceActionType {
|
||||||
|
@ -60,36 +78,104 @@ 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 {
|
||||||
preferenceReader.preferenceStore(self.preferenceStore)
|
preferenceReader.preferenceStore(self.preferenceStore)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
override func unmount(
|
||||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
|
||||||
|
var transaction = transaction
|
||||||
|
transaction.disablesAnimations = false
|
||||||
|
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||||
|
|
||||||
|
mountedChildren.forEach {
|
||||||
|
$0.viewTraits = self.viewTraits
|
||||||
|
$0.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
}
|
||||||
|
|
||||||
if let appearanceAction = view.view as? AppearanceActionType {
|
if let appearanceAction = view.view as? AppearanceActionType {
|
||||||
appearanceAction.disappear?()
|
appearanceAction.disappear?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(with reconciler: StackReconciler<R>) {
|
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||||
|
var transaction = transaction
|
||||||
|
transaction.disablesAnimations = false
|
||||||
|
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||||
|
updateVariadicView()
|
||||||
let element = reconciler.render(compositeView: self)
|
let element = reconciler.render(compositeView: self)
|
||||||
reconciler.reconcile(
|
reconciler.reconcile(
|
||||||
self,
|
self,
|
||||||
with: element,
|
with: element,
|
||||||
|
transaction: transaction,
|
||||||
getElementType: { $0.type },
|
getElementType: { $0.type },
|
||||||
updateChild: {
|
updateChild: {
|
||||||
$0.environmentValues = environmentValues
|
$0.environmentValues = environmentValues
|
||||||
$0.view = AnyView(element)
|
$0.view = AnyView(element)
|
||||||
|
$0.transaction = transaction
|
||||||
},
|
},
|
||||||
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
|
mountChild: {
|
||||||
|
$0.makeMountedView(
|
||||||
|
reconciler.renderer,
|
||||||
|
parentTarget,
|
||||||
|
environmentValues,
|
||||||
|
viewTraits,
|
||||||
|
self
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if let lifecycleActions = view.view as? LifecycleActionType {
|
||||||
|
lifecycleActions.update?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateVariadicView() {
|
||||||
|
if var tree = view.view as? _VariadicView_AnyTree {
|
||||||
|
let elements = ((tree.anyContent.view as? GroupView)?.recursiveChildren ?? [tree.anyContent])
|
||||||
|
.enumerated()
|
||||||
|
.map { (pair: EnumeratedSequence<[AnyView]>.Element) -> _VariadicView_Children.Element in
|
||||||
|
var viewTraits = _ViewTraitStore(values: [:])
|
||||||
|
if let traitModifier = pair.element.view as? _TraitWritingModifierProtocol {
|
||||||
|
traitModifier.modifyViewTraitStore(&viewTraits)
|
||||||
|
}
|
||||||
|
return _VariadicView_Children.Element(
|
||||||
|
view: pair.element,
|
||||||
|
id: AnyHashable(pair.offset),
|
||||||
|
// TODO: Retrieve the ID from the `IDView`. Maybe this should use traits too.
|
||||||
|
viewTraits: viewTraits,
|
||||||
|
onTraitsUpdated: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tree.children = _VariadicView_Children(elements: elements)
|
||||||
|
view.view = tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension GroupView {
|
||||||
|
var recursiveChildren: [AnyView] {
|
||||||
|
var allChildren = [AnyView]()
|
||||||
|
for child in children {
|
||||||
|
if !(child.view is ModifiedContentProtocol),
|
||||||
|
let group = child.view as? GroupView
|
||||||
|
{
|
||||||
|
allChildren.append(contentsOf: group.recursiveChildren)
|
||||||
|
} else {
|
||||||
|
allChildren.append(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allChildren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,35 +85,51 @@ public class MountedElement<R: Renderer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
var mountedChildren = [MountedElement<R>]()
|
var mountedChildren = [MountedElement<R>]()
|
||||||
var environmentValues: EnvironmentValues
|
|
||||||
|
|
||||||
unowned var parent: MountedElement<R>?
|
public var transaction: Transaction = .init(animation: nil)
|
||||||
/// `didSet` on this field propagates the preference changes up the view tree.
|
/// Where this element is the process of mounting/unmounting.
|
||||||
var preferenceStore: _PreferenceStore = .init() {
|
var transitionPhase = TransitionPhase.willMount
|
||||||
didSet {
|
/// The current `UnmountTask` of this element.
|
||||||
parent?.preferenceStore.merge(with: preferenceStore)
|
var unmountTask: UnmountTask<R>?
|
||||||
}
|
|
||||||
}
|
public internal(set) var environmentValues: EnvironmentValues
|
||||||
|
|
||||||
|
private(set) weak var parent: MountedElement<R>?
|
||||||
|
|
||||||
|
var preferenceStore: _PreferenceStore = .init()
|
||||||
|
|
||||||
|
public internal(set) var viewTraits: _ViewTraitStore
|
||||||
|
|
||||||
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
element = .app(app)
|
element = .app(app)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
element = .scene(scene)
|
element = .scene(scene)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
init(
|
||||||
|
_ view: AnyView,
|
||||||
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ viewTraits: _ViewTraitStore,
|
||||||
|
_ parent: MountedElement<R>?
|
||||||
|
) {
|
||||||
element = .view(view)
|
element = .view(view)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
|
self.viewTraits = viewTraits
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
|
connectParentPreferenceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateEnvironment() {
|
func updateEnvironment() {
|
||||||
|
@ -128,19 +144,75 @@ public class MountedElement<R: Renderer> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func connectParentPreferenceStore() {
|
||||||
|
preferenceStore.parent = parent?.preferenceStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You must call `super.prepareForMount` before all other mounting work.
|
||||||
|
func prepareForMount(with transaction: Transaction) {
|
||||||
|
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||||
|
if case let .view(view) = element,
|
||||||
|
view.type is GroupView.Type
|
||||||
|
{
|
||||||
|
transitionPhase = parent?.transitionPhase ?? .normal
|
||||||
|
}
|
||||||
|
// Allow the root of a mount to transition
|
||||||
|
// (if their parent isn't mounting, then they are the root of the mount).
|
||||||
|
if parent?.transitionPhase == .normal {
|
||||||
|
viewTraits.insert(
|
||||||
|
transaction.animation != nil
|
||||||
|
|| _AnyTransitionProxy(viewTraits.transition)
|
||||||
|
.resolve(in: environmentValues)
|
||||||
|
.insertionAnimation != nil,
|
||||||
|
forKey: CanTransitionTraitKey.self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You must call `super.mount` after all other mounting work.
|
||||||
func mount(
|
func mount(
|
||||||
before sibling: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on parent: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction
|
||||||
) {
|
) {
|
||||||
fatalError("implement \(#function) in subclass")
|
// Set the phase to `normal` after finished mounting.
|
||||||
|
transitionPhase = .normal
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmount(with reconciler: StackReconciler<R>) {
|
/// You must call `super.unmount` before all other unmounting work.
|
||||||
fatalError("implement \(#function) in subclass")
|
func unmount(
|
||||||
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
if !(self is MountedHostView<R>) {
|
||||||
|
unmountTask = parentTask?.appendChild()
|
||||||
|
}
|
||||||
|
|
||||||
|
// `GroupView`'s don't really unmount, so let their children transition if the group can.
|
||||||
|
if case let .view(view) = element,
|
||||||
|
view.type is GroupView.Type
|
||||||
|
{
|
||||||
|
transitionPhase = parent?.transitionPhase ?? .normal
|
||||||
|
} else {
|
||||||
|
// Set the phase to `willUnmount` before unmounting.
|
||||||
|
transitionPhase = .willUnmount
|
||||||
|
}
|
||||||
|
// Allow the root of an unmount to transition
|
||||||
|
// (if their parent isn't unmounting, then they are the root of the unmount).
|
||||||
|
if parent?.transitionPhase == .normal {
|
||||||
|
viewTraits.insert(
|
||||||
|
transaction.animation != nil
|
||||||
|
|| _AnyTransitionProxy(viewTraits.transition)
|
||||||
|
.resolve(in: environmentValues)
|
||||||
|
.removalAnimation != nil,
|
||||||
|
forKey: CanTransitionTraitKey.self
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(with reconciler: StackReconciler<R>) {
|
func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||||
fatalError("implement \(#function) in subclass")
|
fatalError("implement \(#function) in subclass")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,16 +294,18 @@ extension TypeInfo {
|
||||||
|
|
||||||
extension AnyView {
|
extension AnyView {
|
||||||
func makeMountedView<R: Renderer>(
|
func makeMountedView<R: Renderer>(
|
||||||
|
_ renderer: R,
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues,
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ viewTraits: _ViewTraitStore,
|
||||||
_ parent: MountedElement<R>?
|
_ parent: MountedElement<R>?
|
||||||
) -> MountedElement<R> {
|
) -> MountedElement<R> {
|
||||||
if type == EmptyView.self {
|
if type == EmptyView.self {
|
||||||
return MountedEmptyView(self, environmentValues, parent)
|
return MountedEmptyView(self, environmentValues, viewTraits, parent)
|
||||||
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
|
} else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
|
||||||
return MountedHostView(self, parentTarget, environmentValues, parent)
|
return MountedHostView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||||
} else {
|
} else {
|
||||||
return MountedCompositeView(self, parentTarget, environmentValues, parent)
|
return MountedCompositeView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,20 @@ final class MountedEmptyView<R: Renderer>: MountedElement<R> {
|
||||||
override func mount(
|
override func mount(
|
||||||
before sibling: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on parent: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
) {}
|
with transaction: Transaction
|
||||||
|
) {
|
||||||
|
super.prepareForMount(with: transaction)
|
||||||
|
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||||
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {}
|
override func unmount(
|
||||||
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
}
|
||||||
|
|
||||||
override func update(with reconciler: StackReconciler<R>) {}
|
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction?) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,19 +33,25 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
_ view: AnyView,
|
_ view: AnyView,
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues,
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ viewTraits: _ViewTraitStore,
|
||||||
_ parent: MountedElement<R>?
|
_ parent: MountedElement<R>?
|
||||||
) {
|
) {
|
||||||
self.parentTarget = parentTarget
|
self.parentTarget = parentTarget
|
||||||
|
|
||||||
super.init(view, environmentValues, parent)
|
super.init(view, environmentValues, viewTraits, parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mount(
|
override func mount(
|
||||||
before sibling: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on parent: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction
|
||||||
) {
|
) {
|
||||||
guard let target = reconciler.renderer?.mountTarget(
|
super.prepareForMount(with: transaction)
|
||||||
|
|
||||||
|
self.transaction = transaction
|
||||||
|
|
||||||
|
guard let target = reconciler.renderer.mountTarget(
|
||||||
before: sibling,
|
before: sibling,
|
||||||
to: parentTarget,
|
to: parentTarget,
|
||||||
with: self
|
with: self
|
||||||
|
@ -56,8 +62,17 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
|
|
||||||
guard !view.children.isEmpty else { return }
|
guard !view.children.isEmpty else { return }
|
||||||
|
|
||||||
|
let isGroupView = view.type is GroupView.Type
|
||||||
|
// Don't allow children to transition their mounting since they aren't individually
|
||||||
|
// appearing (unless its a `GroupView`, which is flattened).
|
||||||
mountedChildren = view.children.map {
|
mountedChildren = view.children.map {
|
||||||
$0.makeMountedView(target, environmentValues, self)
|
$0.makeMountedView(
|
||||||
|
reconciler.renderer,
|
||||||
|
target,
|
||||||
|
environmentValues,
|
||||||
|
isGroupView ? self.viewTraits : .init(),
|
||||||
|
self
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
|
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
|
||||||
|
@ -65,44 +80,73 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
|
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
|
||||||
`GroupView`.
|
`GroupView`.
|
||||||
*/
|
*/
|
||||||
let isGroupView = view.type is GroupView.Type
|
|
||||||
mountedChildren.forEach {
|
mountedChildren.forEach {
|
||||||
$0.mount(before: isGroupView ? sibling : nil, on: self, with: reconciler)
|
$0.mount(before: isGroupView ? sibling : nil, on: self, in: reconciler, with: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
private var parentUnmountTask = UnmountTask<R>()
|
||||||
|
override func unmount(
|
||||||
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
|
||||||
guard let target = target else { return }
|
guard let target = target else { return }
|
||||||
|
|
||||||
reconciler.renderer?.unmount(
|
let task = UnmountHostTask(self, in: reconciler) {
|
||||||
|
self.mountedChildren.forEach {
|
||||||
|
$0.unmount(in: reconciler, with: transaction, parentTask: self.unmountTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.isCancelled = parentTask?.isCancelled ?? false
|
||||||
|
unmountTask = task
|
||||||
|
parentTask?.childTasks.append(task)
|
||||||
|
reconciler.renderer.unmount(
|
||||||
target: target,
|
target: target,
|
||||||
from: parentTarget,
|
from: parentTarget,
|
||||||
with: self
|
with: task
|
||||||
) {
|
)
|
||||||
self.mountedChildren.forEach { $0.unmount(with: reconciler) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(with reconciler: StackReconciler<R>) {
|
/// Stop any unfinished unmounts and complete them without transitions.
|
||||||
|
private func invalidateUnmount() {
|
||||||
|
parentUnmountTask.cancel()
|
||||||
|
parentUnmountTask.completeImmediately()
|
||||||
|
parentUnmountTask = .init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||||
guard let target = target else { return }
|
guard let target = target else { return }
|
||||||
|
|
||||||
|
invalidateUnmount()
|
||||||
|
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
target.view = view
|
target.view = view
|
||||||
reconciler.renderer?.update(target: target, with: self)
|
reconciler.renderer.update(target: target, with: self)
|
||||||
|
|
||||||
var childrenViews = view.children
|
var childrenViews = view.children
|
||||||
|
|
||||||
|
let traits = view.type is GroupView.Type ? viewTraits : .init()
|
||||||
|
|
||||||
switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
|
switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
|
||||||
// if existing children present and new children array is empty
|
// if existing children present and new children array is empty
|
||||||
// then unmount all existing children
|
// then unmount all existing children
|
||||||
case (false, true):
|
case (false, true):
|
||||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
mountedChildren.forEach {
|
||||||
|
$0.unmount(in: reconciler, with: transaction, parentTask: self.parentUnmountTask)
|
||||||
|
}
|
||||||
mountedChildren = []
|
mountedChildren = []
|
||||||
|
|
||||||
// if no existing children then mount all new children
|
// if no existing children then mount all new children
|
||||||
case (true, false):
|
case (true, false):
|
||||||
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) }
|
mountedChildren = childrenViews.map {
|
||||||
mountedChildren.forEach { $0.mount(on: self, with: reconciler) }
|
$0.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||||
|
}
|
||||||
|
mountedChildren.forEach { $0.mount(on: self, in: reconciler, with: transaction) }
|
||||||
|
|
||||||
// if both arrays have items then reconcile by types and keys
|
// if both arrays have items then reconcile by types and keys
|
||||||
case (false, false):
|
case (false, false):
|
||||||
|
@ -117,16 +161,24 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
mountedChild.environmentValues = environmentValues
|
mountedChild.environmentValues = environmentValues
|
||||||
mountedChild.view = childView
|
mountedChild.view = childView
|
||||||
mountedChild.updateEnvironment()
|
mountedChild.updateEnvironment()
|
||||||
mountedChild.update(with: reconciler)
|
mountedChild.update(in: reconciler, with: transaction)
|
||||||
newChild = mountedChild
|
newChild = mountedChild
|
||||||
} else {
|
} else {
|
||||||
/* note the order of operations here: we mount the new child first, use the mounted child
|
/* note the order of operations here: we mount the new child first, use the mounted child
|
||||||
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
|
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
|
||||||
by unmounting it.
|
by unmounting it.
|
||||||
*/
|
*/
|
||||||
newChild = childView.makeMountedView(target, environmentValues, self)
|
newChild = childView.makeMountedView(
|
||||||
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler)
|
reconciler.renderer,
|
||||||
mountedChild.unmount(with: reconciler)
|
target,
|
||||||
|
environmentValues,
|
||||||
|
traits,
|
||||||
|
self
|
||||||
|
)
|
||||||
|
newChild.mount(
|
||||||
|
before: mountedChild.firstDescendantTarget, on: self, in: reconciler, with: transaction
|
||||||
|
)
|
||||||
|
mountedChild.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||||
}
|
}
|
||||||
newChildren.append(newChild)
|
newChildren.append(newChild)
|
||||||
mountedChildren.removeFirst()
|
mountedChildren.removeFirst()
|
||||||
|
@ -137,15 +189,15 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
// unmount remaining `mountedChildren`
|
// unmount remaining `mountedChildren`
|
||||||
if !mountedChildren.isEmpty {
|
if !mountedChildren.isEmpty {
|
||||||
for child in mountedChildren {
|
for child in mountedChildren {
|
||||||
child.unmount(with: reconciler)
|
child.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// more views left than children were mounted,
|
// more views left than children were mounted,
|
||||||
// mount remaining views
|
// mount remaining views
|
||||||
for firstChild in childrenViews {
|
for firstChild in childrenViews {
|
||||||
let newChild: MountedElement<R> =
|
let newChild: MountedElement<R> =
|
||||||
firstChild.makeMountedView(target, environmentValues, self)
|
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||||
newChild.mount(on: self, with: reconciler)
|
newChild.mount(on: self, in: reconciler, with: transaction)
|
||||||
newChildren.append(newChild)
|
newChildren.append(newChild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,25 +31,36 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
||||||
override func mount(
|
override func mount(
|
||||||
before sibling: R.TargetType? = nil,
|
before sibling: R.TargetType? = nil,
|
||||||
on parent: MountedElement<R>? = nil,
|
on parent: MountedElement<R>? = nil,
|
||||||
with reconciler: StackReconciler<R>
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction
|
||||||
) {
|
) {
|
||||||
|
super.prepareForMount(with: transaction)
|
||||||
let childBody = reconciler.render(mountedScene: self)
|
let childBody = reconciler.render(mountedScene: self)
|
||||||
|
|
||||||
let child: MountedElement<R> = childBody
|
let child: MountedElement<R> = childBody
|
||||||
.makeMountedElement(parentTarget, environmentValues, self)
|
.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
|
||||||
mountedChildren = [child]
|
mountedChildren = [child]
|
||||||
child.mount(before: sibling, on: self, with: reconciler)
|
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||||
|
|
||||||
|
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
override func unmount(
|
||||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
in reconciler: StackReconciler<R>,
|
||||||
|
with transaction: Transaction,
|
||||||
|
parentTask: UnmountTask<R>?
|
||||||
|
) {
|
||||||
|
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||||
|
mountedChildren
|
||||||
|
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(with reconciler: StackReconciler<R>) {
|
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||||
let element = reconciler.render(mountedScene: self)
|
let element = reconciler.render(mountedScene: self)
|
||||||
reconciler.reconcile(
|
reconciler.reconcile(
|
||||||
self,
|
self,
|
||||||
with: element,
|
with: element,
|
||||||
|
transaction: transaction,
|
||||||
getElementType: { $0.type },
|
getElementType: { $0.type },
|
||||||
updateChild: {
|
updateChild: {
|
||||||
$0.environmentValues = environmentValues
|
$0.environmentValues = environmentValues
|
||||||
|
@ -59,8 +70,11 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
||||||
case let .view(view):
|
case let .view(view):
|
||||||
$0.view = AnyView(view)
|
$0.view = AnyView(view)
|
||||||
}
|
}
|
||||||
|
$0.transaction = transaction
|
||||||
},
|
},
|
||||||
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) }
|
mountChild: {
|
||||||
|
$0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,21 +90,23 @@ extension _AnyScene.BodyResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMountedElement<R: Renderer>(
|
func makeMountedElement<R: Renderer>(
|
||||||
|
_ renderer: R,
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues,
|
_ environmentValues: EnvironmentValues,
|
||||||
_ parent: MountedElement<R>?
|
_ parent: MountedElement<R>?
|
||||||
) -> MountedElement<R> {
|
) -> MountedElement<R> {
|
||||||
switch self {
|
switch self {
|
||||||
case let .scene(scene):
|
case let .scene(scene):
|
||||||
return scene.makeMountedScene(parentTarget, environmentValues, parent)
|
return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent)
|
||||||
case let .view(view):
|
case let .view(view):
|
||||||
return view.makeMountedView(parentTarget, environmentValues, parent)
|
return view.makeMountedView(renderer, parentTarget, environmentValues, .init(), parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _AnyScene {
|
extension _AnyScene {
|
||||||
func makeMountedScene<R: Renderer>(
|
func makeMountedScene<R: Renderer>(
|
||||||
|
_ renderer: R,
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues,
|
_ environmentValues: EnvironmentValues,
|
||||||
_ parent: MountedElement<R>?
|
_ parent: MountedElement<R>?
|
||||||
|
@ -104,11 +120,17 @@ extension _AnyScene {
|
||||||
let children: [MountedElement<R>]
|
let children: [MountedElement<R>]
|
||||||
if let deferredScene = scene as? SceneDeferredToRenderer {
|
if let deferredScene = scene as? SceneDeferredToRenderer {
|
||||||
children = [
|
children = [
|
||||||
deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent),
|
deferredScene.deferredBody.makeMountedView(
|
||||||
|
renderer,
|
||||||
|
parentTarget,
|
||||||
|
environmentValues,
|
||||||
|
.init(),
|
||||||
|
parent
|
||||||
|
),
|
||||||
]
|
]
|
||||||
} else if let groupScene = scene as? GroupScene {
|
} else if let groupScene = scene as? GroupScene {
|
||||||
children = groupScene.children.map {
|
children = groupScene.children.map {
|
||||||
$0.makeMountedScene(parentTarget, environmentValues, parent)
|
$0.makeMountedScene(renderer, parentTarget, environmentValues, parent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
children = []
|
children = []
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2018-2021 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 7/19/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// A tree of cancellable in-progress unmounts.
|
||||||
|
public class UnmountTask<R> where R: Renderer {
|
||||||
|
public internal(set) var isCancelled = false
|
||||||
|
var childTasks = [UnmountTask<R>]()
|
||||||
|
private let callback: () -> ()
|
||||||
|
|
||||||
|
init(_ callback: @escaping () -> () = {}) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
forEach { $0.isCancelled = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call after completely unmounting the `host`.
|
||||||
|
public func finish() {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds and returns a new child `UnmountTask`
|
||||||
|
func appendChild() -> UnmountTask<R> {
|
||||||
|
let child = UnmountTask()
|
||||||
|
child.isCancelled = isCancelled
|
||||||
|
childTasks.append(child)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forces the element and all child tasks to unmount without transition.
|
||||||
|
func completeImmediately() {
|
||||||
|
forEach {
|
||||||
|
guard $0 is UnmountHostTask<R> else { return }
|
||||||
|
$0.completeImmediately()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forEach(_ f: (UnmountTask<R>) -> ()) {
|
||||||
|
var stack = [self]
|
||||||
|
while let last = stack.popLast() {
|
||||||
|
f(last)
|
||||||
|
stack.insert(contentsOf: last.childTasks, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state for the unmounting of a `MountedHostView` by a `Renderer`.
|
||||||
|
public final class UnmountHostTask<R>: UnmountTask<R> where R: Renderer {
|
||||||
|
public private(set) weak var host: MountedHostView<R>!
|
||||||
|
private unowned var reconciler: StackReconciler<R>
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ host: MountedHostView<R>,
|
||||||
|
in reconciler: StackReconciler<R>,
|
||||||
|
callback: @escaping () -> ()
|
||||||
|
) {
|
||||||
|
self.host = host
|
||||||
|
self.reconciler = reconciler
|
||||||
|
super.init(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func completeImmediately() {
|
||||||
|
host.viewTraits.insert(false, forKey: CanTransitionTraitKey.self)
|
||||||
|
host.unmount(in: reconciler, with: .init(animation: nil), parentTask: nil)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue