Compare commits
206 Commits
transactio
...
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 | |
![]() |
ea94ef9bcc | |
![]() |
53b474c33f | |
![]() |
5d951ad0af | |
![]() |
d75b185553 | |
![]() |
9a79548312 | |
![]() |
30b8d46aa8 | |
![]() |
0e89ea9529 | |
![]() |
07ccef88e1 | |
![]() |
192c43b140 | |
![]() |
e04b7934fb | |
![]() |
9549282e53 | |
![]() |
6f0528fe06 | |
![]() |
67aea3cc3b | |
![]() |
163005dfe0 | |
![]() |
ee4e8debc1 | |
![]() |
9199a90551 | |
![]() |
5ca914818c | |
![]() |
1330b5306b | |
![]() |
6955e56f77 | |
![]() |
e893e7ad8d | |
![]() |
3a0f8a8dd9 | |
![]() |
362be5a5fa | |
![]() |
25e2191154 | |
![]() |
b55f703972 | |
![]() |
a97a05ffd2 | |
![]() |
c9877dcbd7 | |
![]() |
6ef59293f5 | |
![]() |
99bcfd12b9 | |
![]() |
bd38866cb2 | |
![]() |
8e5ad7f67f | |
![]() |
3c97be617a | |
![]() |
ba17b79b1d | |
![]() |
99581929a2 | |
![]() |
302cd3b108 | |
![]() |
8230e98072 | |
![]() |
b4b0efca4d | |
![]() |
e7f295954f | |
![]() |
37cdf6e454 | |
![]() |
8ad964c2f9 | |
![]() |
abceb8609f | |
![]() |
2f97ecfe16 | |
![]() |
1b814d0583 | |
![]() |
797c0d849f | |
![]() |
9d347f49f3 | |
![]() |
2e8e458b9c | |
![]() |
cabe5abef5 | |
![]() |
dfcacc862f | |
![]() |
c754b313ef | |
![]() |
b26eb71f3e | |
![]() |
05465be93d | |
![]() |
d5a50e7045 | |
![]() |
3451d9ea12 | |
![]() |
33adba20ab | |
![]() |
fb3ab974df | |
![]() |
f24a09f006 | |
![]() |
bff7c1bf27 | |
![]() |
c813061b17 | |
![]() |
7320de9857 | |
![]() |
082fa19398 | |
![]() |
18da2d279e | |
![]() |
014383f751 | |
![]() |
af08c1a6f6 | |
![]() |
9681b91a84 | |
![]() |
348408eba1 | |
![]() |
a5da04989e | |
![]() |
94dc934fe4 | |
![]() |
1e43d98bb2 | |
![]() |
af225afab7 | |
![]() |
dbd1ee46c4 | |
![]() |
a631d181e6 | |
![]() |
b5b68c4186 | |
![]() |
ee0006a6a3 | |
![]() |
278201cbd3 | |
![]() |
de72316efa | |
![]() |
ba7af1d014 | |
![]() |
5141bee7d7 | |
![]() |
523c53f14a | |
![]() |
5f450d0e38 | |
![]() |
8ce22014dd | |
![]() |
6affca5931 | |
![]() |
0fe81a060d | |
![]() |
ca5d4fc4ac | |
![]() |
75d178e5dd | |
![]() |
82111c54a0 | |
![]() |
d97a5f3215 | |
![]() |
2383a17c2d | |
![]() |
b9f5ef07ab | |
![]() |
b7434a2e54 | |
![]() |
c43d2db1b3 | |
![]() |
2a49b7808b | |
![]() |
e11effdd8c | |
![]() |
de37894f83 | |
![]() |
c4c9eb595e | |
![]() |
88064fd5bc | |
![]() |
62c05ae9aa | |
![]() |
b93be40a19 | |
![]() |
c7b5e75e1a | |
![]() |
70d31b2e5b | |
![]() |
4c654da456 | |
![]() |
fbb893739b | |
![]() |
050e917161 | |
![]() |
40804d4542 | |
![]() |
e37d13017c |
|
@ -1,6 +1,6 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: MaxDesiatov # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [carson-katri, kateinoigakukun, MaxDesiatov] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: carson-katri
|
||||
|
||||
---
|
||||
|
||||
<!-- Replace the placeholders below with details that help us to reproduce the bug. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
<!-- If this is a layout/rendering issue, please provide a self-contained code snippet that reproduces it. -->
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. macOS 12.4]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version of the browser [e.g. 22]
|
||||
- Version of Tokamak [e.g. 0.10.1]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone 6]
|
||||
- OS: [e.g. iOS15.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version of the browser [e.g. 22]
|
||||
- Version of Tokamak [e.g. 0.10.1]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -1,23 +1,44 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
linux_build:
|
||||
swiftwasm_bundle_5_6:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@master
|
||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
with:
|
||||
shell-action: swift build --triple wasm32-unknown-wasi --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}'
|
||||
|
||||
macos_build:
|
||||
runs-on: macos-10.15
|
||||
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:
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -25,10 +46,78 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo xcode-select --switch /Applications/Xcode_12_beta.app/Contents/Developer/
|
||||
swift test
|
||||
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
|
||||
# avoid building unrelated products for testing by specifying the test product explicitly
|
||||
swift build --product TokamakPackageTests
|
||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
||||
|
||||
rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||
|
||||
xcodebuild -version
|
||||
|
||||
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
xcpretty --color
|
||||
|
||||
cd "NativeDemo"
|
||||
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
xcpretty --color
|
||||
cd ..
|
||||
|
||||
./benchmark.sh
|
||||
|
||||
- name: Upload failed snapshots
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: Failed snapshots
|
||||
path: '*Tests'
|
||||
|
||||
# FIXME: disabled due to build errors, to be investigated
|
||||
# gtk_macos_build:
|
||||
# runs-on: macos-12
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Build the GTK renderer on macOS
|
||||
# shell: bash
|
||||
# run: |
|
||||
# set -ex
|
||||
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
|
||||
#
|
||||
# brew install gtk+3
|
||||
#
|
||||
# make build
|
||||
|
||||
gtk_ubuntu_18_04_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-bionic
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the GTK renderer on Ubuntu 18.04
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
|
||||
|
||||
make build
|
||||
|
||||
gtk_ubuntu_20_04_build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the GTK renderer on Ubuntu 20.04
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
|
||||
|
||||
make build
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
name: Codecov
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
codecov:
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Test Target
|
||||
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||
- name: Run Tests
|
||||
run: swift test --enable-code-coverage --skip-build
|
||||
- name: Generate Branch Coverage Report
|
||||
uses: mattpolzin/swift-codecov-action@0.7.1
|
||||
id: cov
|
||||
with:
|
||||
MINIMUM_COVERAGE: 15
|
||||
- name: Post Positive Results
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- name: Post Negative Results
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
|
@ -1,23 +0,0 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Danger
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the main branch
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
danger-lint:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Danger Swift
|
||||
uses: maxdesiatov/danger-swift@swiftlint-docker
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
|
@ -11,7 +11,7 @@ on:
|
|||
jobs:
|
||||
check-labels:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Match PR Label
|
||||
|
@ -24,5 +24,8 @@ jobs:
|
|||
dependencies,
|
||||
documentation,
|
||||
enhancement,
|
||||
Fiber,
|
||||
refactor,
|
||||
SwiftUI compatibility,
|
||||
test suite,
|
||||
GTK renderer,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
name: SwiftLint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/swiftlint.yml"
|
||||
- ".swiftlint.yml"
|
||||
- "**/*.swift"
|
||||
|
||||
jobs:
|
||||
SwiftLint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- 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
|
||||
uses: mayk-it/action-swiftlint@3.2.2
|
||||
env:
|
||||
DIFF_BASE: ${{ github.base_ref }}
|
||||
DIFF_HEAD: HEAD
|
|
@ -19,6 +19,7 @@ DerivedData
|
|||
*.hmap
|
||||
*.ipa
|
||||
.swiftpm/xcode
|
||||
*.xcodeproj
|
||||
|
||||
# Bundler
|
||||
.bundle
|
||||
|
@ -39,3 +40,7 @@ Pods/
|
|||
|
||||
# SwiftPM
|
||||
.build
|
||||
/Packages
|
||||
|
||||
# VS Code
|
||||
.vscode/launch.json
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
exclude: __Snapshots__
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/hodovani/pre-commit-swift
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/hodovani/pre-commit-swift
|
||||
rev: master
|
||||
hooks:
|
||||
- id: swift-lint
|
||||
- id: swift-format
|
||||
- id: swift-lint
|
||||
- id: swift-format
|
||||
|
|
|
@ -1 +1 @@
|
|||
wasm-DEVELOPMENT-SNAPSHOT-2020-06-12-a
|
||||
wasm-5.6.0-RELEASE
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
--indent 2
|
||||
--indentcase false
|
||||
--trimwhitespace always
|
||||
--empty tuple
|
||||
--voidtype tuple
|
||||
--nospaceoperators ..<,...
|
||||
--ifdef noindent
|
||||
--stripunusedargs closure-only
|
||||
--maxwidth 100
|
||||
--wraparguments before-first
|
||||
--funcattributes prev-line
|
||||
--typeattributes prev-line
|
||||
--varattributes prev-line
|
||||
--disable andOperator
|
||||
--swiftversion 5.2
|
||||
--swiftversion 5.6
|
||||
|
|
|
@ -9,6 +9,7 @@ disabled_rules:
|
|||
- type_name
|
||||
- todo
|
||||
- large_tuple
|
||||
- opening_brace
|
||||
|
||||
line_length: 100
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"licenser.author": "Tokamak contributors"
|
||||
"licenser.author": "Tokamak contributors",
|
||||
"cSpell.words": [
|
||||
"Tokamak"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,12 +11,37 @@
|
|||
{
|
||||
"label": "swift test",
|
||||
"type": "shell",
|
||||
"command": "swift test"
|
||||
"command": "swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest"
|
||||
},
|
||||
{
|
||||
"label": "carton dev",
|
||||
"type": "shell",
|
||||
"command": "carton dev --product TokamakDemo"
|
||||
},
|
||||
{
|
||||
"label": "benchmark",
|
||||
"type": "shell",
|
||||
"command": "./benchmark.sh"
|
||||
},
|
||||
{
|
||||
"label": "make",
|
||||
"type": "shell",
|
||||
"command": "make",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "make run",
|
||||
"type": "shell",
|
||||
"command": "make run",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
436
CHANGELOG.md
436
CHANGELOG.md
|
@ -1,4 +1,438 @@
|
|||
# 0.2.0 (21 July, 2020)
|
||||
# 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)
|
||||
|
||||
This release fixes autocomplete in Xcode for projects that depend on Tokamak.
|
||||
|
||||
# 0.6.0 (4 December 2020)
|
||||
|
||||
This release introduces support for the `Image` view, which can load images bundled as SwiftPM
|
||||
resources. It also adds the `PreferenceKey` protocol and `preference(key:value:)`,
|
||||
`onPreferenceChange`, `backgroundPreferenceValue`, `transformPreference`, and
|
||||
`overlayPreferenceValue` modifiers. Many thanks to [@carson-katri](https://github.com/carson-katri)
|
||||
and [@j-f1](https://github.com/j-f1) for implementing this!
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add [@kateinoigakukun](https://github.com/kateinoigakukun) to the list of maintainers ([#310](https://github.com/TokamakUI/Tokamak/pull/310)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `Image` implementation, bump JSKit to 0.9.0 ([#155](https://github.com/TokamakUI/Tokamak/pull/155)) via [@j-f1](https://github.com/j-f1)
|
||||
- Add Preferences ([#307](https://github.com/TokamakUI/Tokamak/pull/307)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Remove unused Dangerfile.swift ([#311](https://github.com/TokamakUI/Tokamak/pull/311)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.5.3 (28 November 2020)
|
||||
|
||||
A bugfix release that fixes `Toggle` values not updated when reset from a binding. Additionally, the
|
||||
embedded internal implementation of `JSScheduler` is replaced with one from
|
||||
[`OpenCombineJS`](https://github.com/swiftwasm/OpenCombineJS). This library is a new dependency of
|
||||
Tokamak used in the DOM renderer.
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- `Toggle` value not updated when it's reset from a binding ([#287](https://github.com/TokamakUI/Tokamak/issues/287))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix update of `checked` property of checkbox input ([#309](https://github.com/TokamakUI/Tokamak/pull/309)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use latest macOS and Xcode on CI ([#308](https://github.com/TokamakUI/Tokamak/pull/308)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use `JSScheduler` from `OpenCombineJS` package ([#304](https://github.com/TokamakUI/Tokamak/pull/304)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.5.2 (12 November 2020)
|
||||
|
||||
This is a bugfix release that fixes in-tree updates in cases where type of a view changes with
|
||||
conditional updates. Thanks to [@vi4m](https://github.com/vi4m) for reporting the issue!
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Pass sibling to `Renderer.mount`, fix update order ([#301](https://github.com/TokamakUI/Tokamak/pull/301)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.5.1 (9 November 2020)
|
||||
|
||||
A bugfix release to improve compatibility with Xcode autocomplete.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Update Package.resolved ([#300](https://github.com/TokamakUI/Tokamak/pull/300)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
- Allow use of Combine to enable Xcode autocomplete ([#299](https://github.com/TokamakUI/Tokamak/pull/299)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.5.0 (9 November 2020)
|
||||
|
||||
This is a compatibility release with small feature additions. Namely the `Link` view is now available,
|
||||
and our JavaScriptKit dependency has been updated. The latter change now allows you to open
|
||||
`Package.swift` package manifests of your Tokamak projects with working auto-complete in Xcode.
|
||||
Also, our dark mode implementation now more closely follows SwiftUI behavior.
|
||||
|
||||
Many thanks to [@carson-katri](https://github.com/carson-katri) and
|
||||
[@kateinoigakukun](https://github.com/kateinoigakukun) for their contributions to this release!
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Can't build Tokamak project - carton dev command ([#296](https://github.com/TokamakUI/Tokamak/issues/296))
|
||||
- Colors should change depending on light/dark color scheme ([#290](https://github.com/TokamakUI/Tokamak/issues/290))
|
||||
- Pattern for handling global dom events ([#284](https://github.com/TokamakUI/Tokamak/issues/284))
|
||||
- 0.4.0 upgrade / regression? ([#283](https://github.com/TokamakUI/Tokamak/issues/283))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Xcode compatibility ([#297](https://github.com/TokamakUI/Tokamak/pull/297)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
- Allow tests to be run on macOS ([#295](https://github.com/TokamakUI/Tokamak/pull/295)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add Link view, update JavaScriptKit to 0.8.0 ([#276](https://github.com/TokamakUI/Tokamak/pull/276)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `AnyColorBox` and `AnyFontBox` ([#291](https://github.com/TokamakUI/Tokamak/pull/291)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Replace Danger with SwiftLint to improve warnings ([#293](https://github.com/TokamakUI/Tokamak/pull/293)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use v5.3 tag of `swiftwasm-action` in `ci.yml` ([#292](https://github.com/TokamakUI/Tokamak/pull/292)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add @carson-katri and @kateinoigakukun to `FUNDING.yml` ([#289](https://github.com/TokamakUI/Tokamak/pull/289)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `URLHashDemo` w/ `window.onhashchange` closure ([#288](https://github.com/TokamakUI/Tokamak/pull/288)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.4.0 (30 September 2020)
|
||||
|
||||
This is mainly a bugfix and compatibility release with a small feature addition. Namely, `Slider`
|
||||
view is introduced in the DOM renderer, and binding updates for SVG elements are working now. During
|
||||
this development cycle efforts of our team were devoted to recently released [JavaScriptKit
|
||||
0.7](https://github.com/swiftwasm/JavaScriptKit/releases/tag/0.7.0) and [`carton`
|
||||
0.6](https://github.com/swiftwasm/carton/releases/tag/0.6.0). Both of those releases are pretty big
|
||||
updates that improve developer experience significantly, and this version of Tokamak requires those
|
||||
as minimum versions.
|
||||
|
||||
Many thanks to [@j-f1](https://github.com/j-f1) and
|
||||
[@kateinoigakukun](https://github.com/kateinoigakukun) for their contributions to these updates!
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- HTML + Binding ([#278](https://github.com/TokamakUI/Tokamak/issues/278))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix compatibility with JavaScriptKit 0.7 ([#281](https://github.com/TokamakUI/Tokamak/pull/281))
|
||||
via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Re-export `HTML` type in `TokamakDOM` ([#275](https://github.com/TokamakUI/Tokamak/pull/275)) via
|
||||
[@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
- Use setAttribute, not properties to fix SVG update
|
||||
([#279](https://github.com/TokamakUI/Tokamak/pull/279)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Allow non-body mount host node ([#271](https://github.com/TokamakUI/Tokamak/pull/271)) via
|
||||
[@kateinoigakukun](https://github.com/kateinoigakukun)
|
||||
- Add missing JavaScriptKit import to `README.md`
|
||||
([#265](https://github.com/TokamakUI/Tokamak/pull/265)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix the sizing of sliders ([#268](https://github.com/TokamakUI/Tokamak/pull/268)) via
|
||||
[@j-f1](https://github.com/j-f1)
|
||||
- Add `Slider` implementation ([#228](https://github.com/TokamakUI/Tokamak/pull/228)) via
|
||||
[@j-f1](https://github.com/j-f1)
|
||||
- Remove Xcode 12 warning from README.md ([#264](https://github.com/TokamakUI/Tokamak/pull/264)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.3.0 (19 August 2020)
|
||||
|
||||
This release improves compatibility with the SwiftUI API and fixes bugs in our WebAssembly/DOM renderer, included but not limited to:
|
||||
|
||||
- support for `App`/`Scene` lifecycle;
|
||||
- `ColorScheme` detection and environment setting;
|
||||
- dark mode styles;
|
||||
- `@StateObject` property wrapper implementation;
|
||||
- `SidebarListStyle`, `ButtonStyle`, `GeometryProxy` types;
|
||||
- `NavigationView` and `GeometryReader` views.
|
||||
|
||||
Additionally, new `TokamakStaticHTML` renderer was added that supports rendering stateless views into static HTML that doesn't include any JavaScript or WebAssembly dependencies. This is useful for static websites and in the future could be used together with `TokamakDOM` for server-side rendering.
|
||||
|
||||
Tokamak 0.3.0 now requires 5.3 snapshots of SwiftWasm, which in general should be more stable than the development snapshots that were previously used, and is also compatible with Xcode 12 betas. If you have a `.swift-version` file in your project, you should specify `wasm-5.3-SNAPSHOT-2020-07-27-a` in it or a later 5.3 snapshot, otherwise `carton` 0.5 selects a compatible 5.3 snapshot for you automatically. Allowing `carton` to select a default snapshot is the recommended approach, so in general we recommend avoiding `.swif-version` files in projects that use Tokamak.
|
||||
|
||||
Many thanks to [@carson-katri](https://github.com/carson-katri), [@j-f1](https://github.com/j-f1),
|
||||
and [@Outcue](https://github.com/Outcue) for their contributions to this release.
|
||||
|
||||
The complete list of changes included in this release is available below.
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Command "carton dev" failed ([#258](https://github.com/swiftwasm/Tokamak/issues/258))
|
||||
- Dark mode detection causes crashes in Safari
|
||||
([#245](https://github.com/swiftwasm/Tokamak/issues/245))
|
||||
- Add dark color scheme style ([#237](https://github.com/swiftwasm/Tokamak/issues/237))
|
||||
- Establish App lifecycle as the only way to start rendering
|
||||
([#224](https://github.com/swiftwasm/Tokamak/issues/224))
|
||||
- Runtime issues with dynamic properties in `App` types
|
||||
([#222](https://github.com/swiftwasm/Tokamak/issues/222))
|
||||
- `List` appearance changes when reloaded ([#212](https://github.com/swiftwasm/Tokamak/issues/212))
|
||||
- List scrolling does not work on Firefox 78 on macOS
|
||||
([#211](https://github.com/swiftwasm/Tokamak/issues/211))
|
||||
- Scrolling broken when `List` is child of `NavigationView`
|
||||
([#208](https://github.com/swiftwasm/Tokamak/issues/208))
|
||||
- `Rectangle` frame is not being set properly
|
||||
([#185](https://github.com/swiftwasm/Tokamak/issues/185))
|
||||
- Implement `SidebarListStyle` ([#180](https://github.com/swiftwasm/Tokamak/issues/180))
|
||||
- Implement `GeometryReader`/`GeometryProxy`
|
||||
([#176](https://github.com/swiftwasm/Tokamak/issues/176))
|
||||
- `@StateObject` support ([#158](https://github.com/swiftwasm/Tokamak/issues/158))
|
||||
- NavigationView/NavigationLink ([#129](https://github.com/swiftwasm/Tokamak/issues/129))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Set versions of dependencies in `Package.swift`
|
||||
([#262](https://github.com/swiftwasm/Tokamak/pull/262)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Implement `StateObject` property wrapper ([#260](https://github.com/swiftwasm/Tokamak/pull/260))
|
||||
via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix `NavigationView` broken state after re-render
|
||||
([#259](https://github.com/swiftwasm/Tokamak/pull/259)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `GeometryReader` implementation ([#239](https://github.com/swiftwasm/Tokamak/pull/239)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add default dark styles for Views ([#241](https://github.com/swiftwasm/Tokamak/pull/241)) via
|
||||
[@carson-katri](https://github.com/carson-katri)
|
||||
- Link to the renderers guide from `README.md`
|
||||
([#251](https://github.com/swiftwasm/Tokamak/pull/251)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use the latest 5.3 snapshot in `.swift-version`
|
||||
([#252](https://github.com/swiftwasm/Tokamak/pull/252)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix color scheme observer crashes in Safari
|
||||
([#249](https://github.com/swiftwasm/Tokamak/pull/249)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Update to the latest version of SwiftFormat
|
||||
([#250](https://github.com/swiftwasm/Tokamak/pull/250)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Split demo list into sections ([#243](https://github.com/swiftwasm/Tokamak/pull/243)) via
|
||||
[@j-f1](https://github.com/j-f1)
|
||||
- Remove some `AnyView` in the `List` implementation
|
||||
([#246](https://github.com/swiftwasm/Tokamak/pull/246)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `_targetRef` and `_domRef` modifiers ([#240](https://github.com/swiftwasm/Tokamak/pull/240))
|
||||
via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `ColorScheme` environment ([#136](https://github.com/swiftwasm/Tokamak/pull/136)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `redacted` modifier ([#232](https://github.com/swiftwasm/Tokamak/pull/232)) via
|
||||
[@carson-katri](https://github.com/carson-katri)
|
||||
- Add Static HTML Renderer and Documentation ([#204](https://github.com/swiftwasm/Tokamak/pull/204))
|
||||
via [@carson-katri](https://github.com/carson-katri)
|
||||
- Fix tests, move `DefaultButtonStyle` to TokamakCore
|
||||
([#234](https://github.com/swiftwasm/Tokamak/pull/234)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Remove `DefaultApp`, make `DOMRenderer` internal
|
||||
([#227](https://github.com/swiftwasm/Tokamak/pull/227)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add basic `ButtonStyle` implementation ([#214](https://github.com/swiftwasm/Tokamak/pull/214)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Make reconciler tests build and run on macOS
|
||||
([#229](https://github.com/swiftwasm/Tokamak/pull/229)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix environment changes causing remounted scenes with lost state
|
||||
([#223](https://github.com/swiftwasm/Tokamak/pull/223)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `DefaultApp` type to simplify `DOMRenderer.init`
|
||||
([#217](https://github.com/swiftwasm/Tokamak/pull/217)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Implement `SidebarListStyle` ([#210](https://github.com/swiftwasm/Tokamak/pull/210)) via
|
||||
[@Outcue](https://github.com/Outcue)
|
||||
- Unify code of `MountedApp`/`MountedCompositeView`
|
||||
([#219](https://github.com/swiftwasm/Tokamak/pull/219)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Generalize style and environment in `DOMRenderer`
|
||||
([#215](https://github.com/swiftwasm/Tokamak/pull/215)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Implement `DynamicProperty` ([#213](https://github.com/swiftwasm/Tokamak/pull/213)) via
|
||||
[@carson-katri](https://github.com/carson-katri)
|
||||
- Warn against beta versions of Xcode in README.md
|
||||
([#207](https://github.com/swiftwasm/Tokamak/pull/207)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix typo in `TokamakDemo.swift` ([#206](https://github.com/swiftwasm/Tokamak/pull/206)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Update "Requirements" and "Getting started" README sections
|
||||
([#205](https://github.com/swiftwasm/Tokamak/pull/205)) via
|
||||
[@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Initial `NavigationView` implementation ([#130](https://github.com/swiftwasm/Tokamak/pull/130))
|
||||
via [@j-f1](https://github.com/j-f1)
|
||||
- Add SwiftUI App Lifecycle ([#195](https://github.com/swiftwasm/Tokamak/pull/195)) via
|
||||
[@carson-katri](https://github.com/carson-katri)
|
||||
|
||||
# 0.2.0 (21 July 2020)
|
||||
|
||||
This is the first release that supports WebAssembly and browser apps with the new `TokamakDOM`
|
||||
module. The API now closely follows SwiftUI, while the new React-like API is no longer available.
|
||||
|
|
|
@ -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
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
LINKER_FLAGS := $(shell pkg-config --libs gtk+-3.0 gdk-3.0)
|
||||
C_FLAGS := $(shell pkg-config --cflags gtk+-3.0)
|
||||
SWIFT_LINKER_FLAGS ?= -Xlinker $(shell echo $(LINKER_FLAGS) | sed -e "s/ / -Xlinker /g" | sed -e "s/-Xlinker -Wl,-framework,/-Xlinker -framework -Xlinker /g")
|
||||
SWIFT_C_FLAGS ?= -Xcc $(shell echo $(C_FLAGS) | sed -e "s/ / -Xcc /g")
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
swift build --enable-test-discovery --product TokamakGTKDemo $(SWIFT_C_FLAGS) $(SWIFT_LINKER_FLAGS)
|
||||
|
||||
run: build
|
||||
.build/debug/TokamakGTKDemo
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -7,45 +7,75 @@
|
|||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
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 */; };
|
||||
85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.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 */; };
|
||||
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 */; };
|
||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
||||
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
|
||||
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
|
||||
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
|
||||
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
|
||||
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
||||
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
||||
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
|
||||
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
|
||||
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
|
||||
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
|
||||
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
|
||||
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
|
||||
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
|
||||
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
|
||||
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
|
||||
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
|
||||
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
|
||||
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
|
||||
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
|
||||
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
|
||||
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
|
||||
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
|
||||
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
|
||||
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
|
||||
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
|
||||
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
|
||||
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.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 */; };
|
||||
D1E5FDAF24C1D58E00E7485E /* 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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -78,32 +108,48 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
|
||||
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
|
||||
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
|
||||
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>"; };
|
||||
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
|
||||
B56F22DF24BC89FD001738DF /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
|
||||
B56F22E224BD1C26001738DF /* 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>"; };
|
||||
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
|
||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
|
||||
D1078726274BD1E5003E787B /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
|
||||
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
|
||||
D1078728274BD1E5003E787B /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
|
||||
D1078729274BD1E5003E787B /* StackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackDemo.swift; sourceTree = "<group>"; };
|
||||
D107872B274BD1E5003E787B /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
|
||||
D107872C274BD1E5003E787B /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
|
||||
D107872D274BD1E5003E787B /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
|
||||
D107872E274BD1E5003E787B /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
|
||||
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
|
||||
D1078731274BD1E5003E787B /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.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; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -136,6 +182,7 @@
|
|||
85ED183D24AD37970085DFA0 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */,
|
||||
D1E5FDAB24C1D57000E7485E /* TokamakShim */,
|
||||
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */,
|
||||
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */,
|
||||
|
@ -162,27 +209,106 @@
|
|||
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
|
||||
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
|
||||
85ED189E24AD425E0085DFA0 /* Counter.swift */,
|
||||
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */,
|
||||
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */,
|
||||
B56F22E224BD1C26001738DF /* GridDemo.swift */,
|
||||
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */,
|
||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
|
||||
B51F214F24B920B400CF2583 /* PathDemo.swift */,
|
||||
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
|
||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
|
||||
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */,
|
||||
85ED189B24AD425E0085DFA0 /* TextDemo.swift */,
|
||||
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
|
||||
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */,
|
||||
D107874B274BD1E5003E787B /* Buttons */,
|
||||
D107872F274BD1E5003E787B /* Containers */,
|
||||
D107873A274BD1E5003E787B /* Drawing */,
|
||||
D1078725274BD1E5003E787B /* Layout */,
|
||||
D107873F274BD1E5003E787B /* Misc */,
|
||||
D1078734274BD1E5003E787B /* Modifiers */,
|
||||
D107872A274BD1E5003E787B /* Selectors */,
|
||||
D1078747274BD1E5003E787B /* Text */,
|
||||
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */,
|
||||
);
|
||||
name = TokamakDemo;
|
||||
path = ../Sources/TokamakDemo;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -321,23 +447,38 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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 */,
|
||||
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */,
|
||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.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 */,
|
||||
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
|
||||
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */,
|
||||
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
||||
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
||||
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
||||
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
||||
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
||||
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */,
|
||||
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
||||
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
|
||||
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */,
|
||||
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */,
|
||||
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */,
|
||||
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
|
||||
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */,
|
||||
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
|
||||
D1078772274BD1E5003E787B /* ColorDemo.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;
|
||||
};
|
||||
|
@ -345,23 +486,38 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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 */,
|
||||
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */,
|
||||
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.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 */,
|
||||
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
|
||||
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */,
|
||||
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
|
||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
||||
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
||||
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
|
||||
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
||||
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */,
|
||||
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
||||
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
||||
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */,
|
||||
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */,
|
||||
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */,
|
||||
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
|
||||
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */,
|
||||
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
|
||||
D1078773274BD1E5003E787B /* ColorDemo.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;
|
||||
};
|
||||
|
@ -509,17 +665,22 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 288H3WAR3W;
|
||||
CODE_SIGN_ENTITLEMENTS = "TokamakDemo Native.entitlements";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = "iOS Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
|
||||
PRODUCT_NAME = "TokamakDemo Native";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -531,17 +692,22 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 288H3WAR3W;
|
||||
CODE_SIGN_ENTITLEMENTS = "TokamakDemo Native.entitlements";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = "iOS Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
|
||||
PRODUCT_NAME = "TokamakDemo Native";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
@ -554,7 +720,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
@ -564,7 +730,10 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
|
||||
PRODUCT_NAME = "TokamakDemo Native";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
@ -578,9 +747,9 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 288H3WAR3W;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
|
@ -588,7 +757,10 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
|
||||
PRODUCT_NAME = "TokamakDemo Native";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
|
@ -1,34 +1,59 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "JavaScriptKit",
|
||||
"repositoryURL": "https://github.com/kateinoigakukun/JavaScriptKit.git",
|
||||
"state": {
|
||||
"branch": "c90e82f",
|
||||
"revision": "c90e82fe1d576a2ccd1aae798380bf80be7885fb",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombine",
|
||||
"repositoryURL": "https://github.com/MaxDesiatov/OpenCombine.git",
|
||||
"state": {
|
||||
"branch": "observable-object",
|
||||
"revision": "3c3a181acad7ab44a64d7c41140eb843222bb2aa",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Runtime",
|
||||
"repositoryURL": "https://github.com/MaxDesiatov/Runtime.git",
|
||||
"state": {
|
||||
"branch": "wasi-build",
|
||||
"revision": "a9309b4822d6dd0e4a8e92351ee9e3d210e19b4e",
|
||||
"version": null
|
||||
}
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "javascriptkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
"state" : {
|
||||
"revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
|
||||
"version" : "0.15.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
|
||||
}
|
||||
|
|
216
Package.swift
216
Package.swift
|
@ -1,13 +1,11 @@
|
|||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to
|
||||
// build this package.
|
||||
// swift-tools-version:5.6
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Tokamak",
|
||||
platforms: [
|
||||
.macOS(.v10_15),
|
||||
.macOS(.v11),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
|
@ -21,54 +19,216 @@ let package = Package(
|
|||
name: "TokamakDOM",
|
||||
targets: ["TokamakDOM"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakStaticHTML",
|
||||
targets: ["TokamakStaticHTML"]
|
||||
),
|
||||
.executable(
|
||||
name: "TokamakStaticHTMLDemo",
|
||||
targets: ["TokamakStaticHTMLDemo"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakGTK",
|
||||
targets: ["TokamakGTK"]
|
||||
),
|
||||
.executable(
|
||||
name: "TokamakGTKDemo",
|
||||
targets: ["TokamakGTKDemo"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakShim",
|
||||
targets: ["TokamakShim"]
|
||||
),
|
||||
.executable(
|
||||
name: "TokamakStaticHTMLBenchmark",
|
||||
targets: ["TokamakStaticHTMLBenchmark"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/kateinoigakukun/JavaScriptKit.git", .revision("c90e82f")),
|
||||
.package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build")),
|
||||
.package(url: "https://github.com/MaxDesiatov/OpenCombine.git", .branch("observable-object")),
|
||||
.package(
|
||||
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
|
||||
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"
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define
|
||||
// a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products
|
||||
// in packages which this package depends on.
|
||||
.target(
|
||||
name: "CombineShim",
|
||||
dependencies: [.product(
|
||||
name: "OpenCombine",
|
||||
package: "OpenCombine",
|
||||
condition: .when(platforms: [.wasi, .linux])
|
||||
)]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakCore",
|
||||
dependencies: ["CombineShim", "Runtime"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDemo",
|
||||
dependencies: ["JavaScriptKit", "TokamakShim"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDOM",
|
||||
dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore"]
|
||||
dependencies: [
|
||||
.product(
|
||||
name: "OpenCombineShim",
|
||||
package: "OpenCombine"
|
||||
),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakShim",
|
||||
dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))]
|
||||
dependencies: [
|
||||
.target(name: "TokamakDOM", condition: .when(platforms: [.wasi])),
|
||||
.target(name: "TokamakGTK", condition: .when(platforms: [.linux])),
|
||||
]
|
||||
),
|
||||
.systemLibrary(
|
||||
name: "CGTK",
|
||||
pkgConfig: "gtk+-3.0",
|
||||
providers: [
|
||||
.apt(["libgtk+-3.0", "gtk+-3.0"]),
|
||||
// .yum(["gtk3-devel"]),
|
||||
.brew(["gtk+3"]),
|
||||
]
|
||||
),
|
||||
.systemLibrary(
|
||||
name: "CGDK",
|
||||
pkgConfig: "gdk-3.0",
|
||||
providers: [
|
||||
.apt(["libgtk+-3.0", "gtk+-3.0"]),
|
||||
// .yum(["gtk3-devel"]),
|
||||
.brew(["gtk+3"]),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakGTKCHelpers",
|
||||
dependencies: ["CGTK"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakGTK",
|
||||
dependencies: [
|
||||
"TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers",
|
||||
.product(
|
||||
name: "OpenCombineShim",
|
||||
package: "OpenCombine"
|
||||
),
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "TokamakGTKDemo",
|
||||
dependencies: ["TokamakGTK"],
|
||||
resources: [.copy("logo-header.png")]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakStaticHTML",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "TokamakCoreBenchmark",
|
||||
dependencies: [
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "TokamakStaticHTMLBenchmark",
|
||||
dependencies: [
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakStaticHTML",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDOM",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakStaticHTML",
|
||||
.product(
|
||||
name: "OpenCombineShim",
|
||||
package: "OpenCombine"
|
||||
),
|
||||
.product(
|
||||
name: "JavaScriptKit",
|
||||
package: "JavaScriptKit",
|
||||
condition: .when(platforms: [.wasi])
|
||||
),
|
||||
.product(
|
||||
name: "JavaScriptEventLoop",
|
||||
package: "JavaScriptKit",
|
||||
condition: .when(platforms: [.wasi])
|
||||
),
|
||||
"OpenCombineJS",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "TokamakDemo",
|
||||
dependencies: [
|
||||
"TokamakShim",
|
||||
.product(
|
||||
name: "JavaScriptKit",
|
||||
package: "JavaScriptKit",
|
||||
condition: .when(platforms: [.wasi])
|
||||
),
|
||||
],
|
||||
resources: [.copy("logo-header.png")],
|
||||
linkerSettings: [
|
||||
.unsafeFlags(
|
||||
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
|
||||
.when(platforms: [.wasi])
|
||||
),
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "TokamakStaticHTMLDemo",
|
||||
dependencies: [
|
||||
"TokamakStaticHTML",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakTestRenderer",
|
||||
dependencies: ["TokamakCore"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakLayoutTests",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakStaticHTML",
|
||||
.product(
|
||||
name: "SnapshotTesting",
|
||||
package: "swift-snapshot-testing",
|
||||
condition: .when(platforms: [.macOS])
|
||||
),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakReconcilerTests",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakTests",
|
||||
dependencies: ["TokamakDemo", "TokamakTestRenderer"]
|
||||
dependencies: ["TokamakTestRenderer"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakStaticHTMLTests",
|
||||
dependencies: [
|
||||
"TokamakStaticHTML",
|
||||
.product(
|
||||
name: "SnapshotTesting",
|
||||
package: "swift-snapshot-testing",
|
||||
condition: .when(platforms: [.macOS])
|
||||
),
|
||||
],
|
||||
exclude: ["__Snapshots__", "RenderingTests/__Snapshots__"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
316
README.md
316
README.md
|
@ -1,25 +1,28 @@
|
|||
<img alt="Tokamak logo" src="docs/logo-header.png" width="640px"/>
|
||||
<img alt="Tokamak logo" src="Sources/TokamakDemo/logo-header.png" width="640px"/>
|
||||
|
||||
## SwiftUI-compatible framework for building browser apps with WebAssembly
|
||||
|
||||

|
||||
[](https://github.com/TokamakUI/Tokamak/actions?query=workflow%3ACI) [](https://discord.gg/ashJW8T8yp)
|
||||
|
||||
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports
|
||||
a few view types and modifiers (you can check the current list in [the progress document](docs/progress.md)),
|
||||
and a new `HTML` view for constructing arbitrary HTML. The long-term goal of Tokamak is to implement
|
||||
as much of SwiftUI API as possible and to provide a few more helpful additions that simplify HTML
|
||||
and CSS interactions.
|
||||
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports a few
|
||||
view types and modifiers (you can check the current list in [the progress
|
||||
document](docs/progress.md)), and a new `HTML` view for constructing arbitrary HTML. The long-term
|
||||
goal of Tokamak is to implement as much of SwiftUI API as possible and to provide a few more helpful
|
||||
additions that simplify HTML and CSS interactions.
|
||||
|
||||
If there's some SwiftUI API that's missing but you'd like to use it, please review the existing
|
||||
[issues](https://github.com/swiftwasm/Tokamak/issues) and [PRs](https://github.com/swiftwasm/Tokamak/pulls)
|
||||
to get more details about the current status, or [create a new issue](https://github.com/swiftwasm/Tokamak/issues/new)
|
||||
to let us prioritize the development based on the demand. We also try to make the development of
|
||||
views and modifiers easier (with the help from the `HTML` view, see [the example
|
||||
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome! Don't
|
||||
forget to check [the "Contributing" section](https://github.com/swiftwasm/Tokamak#contributing) first.
|
||||
[issues](https://github.com/swiftwasm/Tokamak/issues) and
|
||||
[PRs](https://github.com/swiftwasm/Tokamak/pulls) to get more details about the current status, or
|
||||
[create a new issue](https://github.com/swiftwasm/Tokamak/issues/new) to let us prioritize the
|
||||
development based on the demand. We also try to make the development of views and modifiers easier
|
||||
(with the help from the `HTML` view, see [the example
|
||||
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome!
|
||||
Don't forget to check [the "Contributing"
|
||||
section](https://github.com/swiftwasm/Tokamak#contributing) first.
|
||||
|
||||
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're also very
|
||||
welcome to join the `#webassembly` channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/).
|
||||
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're
|
||||
also very welcome to join [our Discord server](https://discord.gg/ashJW8T8yp), or the `#webassembly`
|
||||
channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/).
|
||||
|
||||
### Example code
|
||||
|
||||
|
@ -48,23 +51,15 @@ struct Counter: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can then render your view in any DOM node captured with
|
||||
[JavaScriptKit](https://github.com/kateinoigakukun/JavaScriptKit/), just
|
||||
pass it as an argument to the `DOMRenderer` initializer together with your view:
|
||||
|
||||
```swift
|
||||
import JavaScriptKit
|
||||
import TokamakDOM
|
||||
|
||||
let document = JSObjectRef.global.document.object!
|
||||
|
||||
let divElement = document.createElement!("div").object!
|
||||
let renderer = DOMRenderer(Counter(count: 5, limit: 15), divElement)
|
||||
|
||||
let body = document.body.object!
|
||||
_ = body.appendChild!(divElement)
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup("Counter Demo") {
|
||||
Counter(count: 5, limit: 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Arbitrary HTML
|
||||
|
@ -84,17 +79,55 @@ 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
|
||||
|
||||
While `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,
|
||||
sometimes you need to inject arbitrary scripts or styles, which can be done through direct
|
||||
DOM access:
|
||||
|
||||
```swift
|
||||
_ = document.head.object!.insertAdjacentHTML!("beforeend", #"""
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js"></script>
|
||||
"""#)
|
||||
_ = document.head.object!.insertAdjacentHTML!("beforeend", #"""
|
||||
import JavaScriptKit
|
||||
|
||||
let document = JSObject.global.document
|
||||
let script = document.createElement("script")
|
||||
script.setAttribute("src", "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js")
|
||||
document.head.appendChild(script)
|
||||
|
||||
_ = document.head.insertAdjacentHTML("beforeend", #"""
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
|
||||
|
@ -105,30 +138,69 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
|
|||
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
||||
app.
|
||||
|
||||
## Requirements for app developers
|
||||
### Fiber renderers
|
||||
|
||||
- macOS 10.15 and Xcode 11.4/11.5/11.6 for macOS. Xcode betas are currently not supported. You can have
|
||||
those installed, but please make sure you use
|
||||
[`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_)
|
||||
to point it to a release version of Xcode.
|
||||
- [Swift 5.2 or later](https://swift.org/download/) for Linux.
|
||||
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
||||
is optionally available. It can provide faster updates and allow for larger View hierarchies.
|
||||
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
|
||||
|
||||
## Requirements for app users
|
||||
You can specify which reconciler to use in your `App`'s configuration:
|
||||
|
||||
Any browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) should work, which currently includes:
|
||||
```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.
|
||||
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
||||
|
||||
### For users of apps depending on Tokamak
|
||||
|
||||
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+
|
||||
- Firefox 53+
|
||||
- Chrome 57+
|
||||
- (Mobile) Safari 11+
|
||||
- Firefox 61+
|
||||
- Chrome 66+
|
||||
- (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
|
||||
|
||||
Tokamak relies on [`carton`](https://carton.dev) as a primary build tool. As a part of these steps
|
||||
you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (unfortunately you'll have to build
|
||||
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
|
||||
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
|
||||
app by following these steps:
|
||||
|
||||
1. Install `carton`:
|
||||
|
@ -137,7 +209,7 @@ app by following these steps:
|
|||
brew install swiftwasm/tap/carton
|
||||
```
|
||||
|
||||
If you had `carton` installed before this, make sure you have version 0.4.1 or greater:
|
||||
If you had `carton` installed before this, make sure you have version 0.15.0 or greater:
|
||||
|
||||
```
|
||||
carton --version
|
||||
|
@ -156,79 +228,82 @@ carton init --template tokamak
|
|||
```
|
||||
|
||||
4. Build the project and start the development server, `carton dev` can be kept running
|
||||
during development:
|
||||
during development:
|
||||
|
||||
```
|
||||
carton dev
|
||||
```
|
||||
|
||||
5. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser to see the app
|
||||
running. You can edit the app source code in your favorite editor and save it, `carton`
|
||||
will immediately rebuild the app and reload all browser tabs that have the app open.
|
||||
running. You can edit the app source code in your favorite editor and save it, `carton`
|
||||
will immediately rebuild the app and reload all browser tabs that have the app open.
|
||||
|
||||
You can also clone this repository and run `carton dev` in its root directory. This
|
||||
will build the demo app that shows almost all of the currently implemented APIs.
|
||||
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
||||
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
||||
|
||||
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
|
||||
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
|
||||
|
||||
## Security
|
||||
|
||||
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
||||
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
|
||||
|
||||
### `unable to find utility "xctest"` error when building
|
||||
|
||||
This error can only happen on macOS, so make sure you have Xcode installed as listed [in the
|
||||
requirements](#requirements-for-app-developers). If you do have Xcode installed but still get the
|
||||
error, please refer to [this StackOverflow answer](https://stackoverflow.com/a/61725799/442427).
|
||||
|
||||
### Syntax highlighting and autocomplete don't work in Xcode
|
||||
|
||||
Open `Package.swift` of your project that depends on Tokamak with Xcode and build it for macOS.
|
||||
As Xcode currently doesn't support cross-compilation for non-Apple platforms, your project can't
|
||||
be indexed if it doesn't build for macOS, even if it isn't fully function on macOS when running.
|
||||
If you need to exclude some WebAssembly-specific code in your own app that doesn't compile on macOS,
|
||||
you can rely on `#if os(WASI)` compiler directives.
|
||||
|
||||
All relevant modules of Tokamak (including `TokamakDOM`) should compile on macOS. You may see issues
|
||||
with `TokamakShim` on macOS Catalina, where relevant SwiftUI APIs aren't supported, but replacing
|
||||
`import TokamakShim` with `import TokamakDOM` should resolve the issue until you're able to update
|
||||
to macOS Big Sur.
|
||||
|
||||
If you stumble upon code in Tokamak that doesn't build on macOS and prevents syntax highlighting or
|
||||
autocomplete from working in Xcode, please [report it as a
|
||||
bug](https://github.com/TokamakUI/Tokamak/issues/new).
|
||||
|
||||
### Syntax highlighting and autocomplete don't work in VSCode
|
||||
|
||||
Make sure you have [the SourceKit LSP
|
||||
extension](https://marketplace.visualstudio.com/items?itemName=pvasek.sourcekit-lsp--dev-unofficial)
|
||||
installed. If you don't trust this unofficial release, please follow [the manual building and
|
||||
installation guide](https://github.com/apple/sourcekit-lsp/tree/main/Editors/vscode). Apple currently
|
||||
doesn't provide an official build of the extension on the VSCode Marketplace unfortunately.
|
||||
|
||||
## 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 cross-platform `TokamakCore` module and
|
||||
separate modules for platform-specific renderers. Currently, the only available renderer module
|
||||
is `TokamakDOM`, but we intend to provide other renderers in the future, such as `TokamakHTML`
|
||||
for static websites and server-side rendering. 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 `TokamakHTML`,
|
||||
single-page apps would use `TokamakDOM`, maybe in conjuction with `TokamakHTML` 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.
|
||||
|
||||
### Sponsorship
|
||||
|
||||
If this library saved you any amount of time or money, please consider [sponsoring
|
||||
the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the
|
||||
sponsorship tiers give you priority support or even consulting time, any amount is
|
||||
appreciated and helps in maintaining the project.
|
||||
|
||||
### Coding Style
|
||||
|
||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
|
||||
and [SwiftLint](https://github.com/realm/SwiftLint) to
|
||||
enforce formatting and coding style. We encourage you to run SwiftFormat within
|
||||
a local clone of the repository in whatever way works best for you either
|
||||
manually or automatically via an [Xcode
|
||||
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension),
|
||||
[build phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase) or
|
||||
[git pre-commit
|
||||
hook](https://github.com/nicklockwood/SwiftFormat#git-pre-commit-hook) etc.
|
||||
|
||||
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.
|
||||
Updating our [documentation](https://github.com/TokamakUI/Tokamak/tree/main/docs) and taking on [the starter
|
||||
bugs](https://github.com/TokamakUI/Tokamak/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
is also appreciated. Don't forget to join our [Discord server](https://discord.gg/ashJW8T8yp) to get in
|
||||
touch with the maintainers and other users. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
|
@ -237,10 +312,23 @@ Conduct](https://github.com/swiftwasm/Tokamak/blob/main/CODE_OF_CONDUCT.md).
|
|||
By participating, you are expected to uphold this code. Please report
|
||||
unacceptable behavior to conduct@tokamak.dev.
|
||||
|
||||
### Sponsorship
|
||||
|
||||
If this library saved you any amount of time or money, please consider sponsoring
|
||||
the work of its maintainers on their sponsorship pages:
|
||||
[@carson-katri](https://github.com/sponsors/carson-katri),
|
||||
[@kateinoigakukun](https://github.com/sponsors/kateinoigakukun), and
|
||||
[@MaxDesiatov](https://github.com/sponsors/MaxDesiatov). While some of the
|
||||
sponsorship tiers give you priority support or even consulting time, any amount is
|
||||
appreciated and helps in maintaining the project.
|
||||
|
||||
## Maintainers
|
||||
|
||||
[Carson Katri](https://github.com/carson-katri),
|
||||
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com).
|
||||
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
||||
[Ezra Berch](https://github.com/ezraberch),
|
||||
[Jed Fox](https://jedfox.com),
|
||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
|
||||
[Yuta Saito](https://github.com/kateinoigakukun/).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
#include <gdk/gdk.h>
|
|
@ -0,0 +1,8 @@
|
|||
module CGDK {
|
||||
header "./termios-Header.h"
|
||||
header "./CGDK-Bridging-Header.h"
|
||||
|
||||
link "gdk-3"
|
||||
|
||||
export *
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
#include <termios.h>
|
|
@ -0,0 +1 @@
|
|||
#include <gtk/gtk.h>
|
|
@ -0,0 +1,8 @@
|
|||
module CGTK {
|
||||
header "./termios-Header.h"
|
||||
header "./CGTK-Bridging-Header.h"
|
||||
|
||||
link "gtk-3"
|
||||
|
||||
export *
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
#include <termios.h>
|
|
@ -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.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/11/21.
|
||||
//
|
||||
|
||||
#if canImport(Combine)
|
||||
@_exported import Combine
|
||||
#else
|
||||
@_exported import OpenCombine
|
||||
#endif
|
||||
public protocol AnimatableModifier: Animatable, ViewModifier {}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,8 +15,7 @@
|
|||
// Created by Carson Katri on 7/16/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import Runtime
|
||||
import OpenCombineShim
|
||||
|
||||
/// Provides the ability to set the title of the Scene.
|
||||
public protocol _TitledApp {
|
||||
|
@ -29,19 +28,49 @@ public protocol App: _TitledApp {
|
|||
var body: Body { get }
|
||||
|
||||
/// 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
|
||||
var _phasePublisher: CurrentValueSubject<ScenePhase, Never> { get }
|
||||
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
||||
|
||||
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||
|
||||
static var _configuration: _AppConfiguration { get }
|
||||
|
||||
static func main()
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
extension App {
|
||||
public static func main() {
|
||||
let app = Self()
|
||||
_launch(app, EnvironmentValues())
|
||||
public struct _AppConfiguration {
|
||||
public let reconciler: Reconciler
|
||||
public let rootEnvironment: EnvironmentValues
|
||||
|
||||
public init(
|
||||
reconciler: Reconciler = .stack,
|
||||
rootEnvironment: EnvironmentValues = .init()
|
||||
) {
|
||||
self.reconciler = reconciler
|
||||
self.rootEnvironment = rootEnvironment
|
||||
}
|
||||
|
||||
public enum Reconciler {
|
||||
/// Use the `StackReconciler`.
|
||||
case stack
|
||||
/// Use the `FiberReconciler` with layout steps optionally enabled.
|
||||
case fiber(useDynamicLayout: Bool = false)
|
||||
}
|
||||
}
|
||||
|
||||
public extension App {
|
||||
static var _configuration: _AppConfiguration { .init() }
|
||||
|
||||
static func main() {
|
||||
let app = Self()
|
||||
_launch(app, with: Self._configuration)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,11 +15,15 @@
|
|||
// 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?
|
||||
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
|
||||
|
||||
@Environment(\._defaultAppStorage)
|
||||
var defaultProvider: _StorageProvider?
|
||||
|
||||
var unwrappedProvider: _StorageProvider {
|
||||
provider ?? defaultProvider!
|
||||
}
|
||||
|
@ -53,10 +57,10 @@ import CombineShim
|
|||
|
||||
extension AppStorage: ObservedProperty {}
|
||||
|
||||
extension AppStorage {
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Bool {
|
||||
public extension AppStorage {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Bool
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -64,9 +68,9 @@ extension AppStorage {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Int {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Int
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -74,9 +78,9 @@ extension AppStorage {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Double {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Double
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -84,9 +88,9 @@ extension AppStorage {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == String {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == String
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -94,10 +98,9 @@ extension AppStorage {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil)
|
||||
where Value: RawRepresentable, Value.RawValue == Int {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value: RawRepresentable, Value.RawValue == Int
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -110,10 +113,9 @@ extension AppStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil)
|
||||
where Value: RawRepresentable, Value.RawValue == String {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value: RawRepresentable, Value.RawValue == String
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -127,10 +129,10 @@ extension AppStorage {
|
|||
}
|
||||
}
|
||||
|
||||
extension AppStorage where Value: ExpressibleByNilLiteral {
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Bool? {
|
||||
public extension AppStorage where Value: ExpressibleByNilLiteral {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Bool?
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -138,9 +140,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Int? {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Int?
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -148,9 +150,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == Double? {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == Double?
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -158,9 +160,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
|
|||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String,
|
||||
store: _StorageProvider? = nil) where Value == String? {
|
||||
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
|
||||
where Value == String?
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
provider = store
|
||||
|
@ -174,8 +176,9 @@ struct DefaultAppStorageEnvironmentKey: EnvironmentKey {
|
|||
static let defaultValue: _StorageProvider? = nil
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
public var _defaultAppStorage: _StorageProvider? {
|
||||
public extension EnvironmentValues {
|
||||
@_spi(TokamakCore)
|
||||
var _defaultAppStorage: _StorageProvider? {
|
||||
get {
|
||||
self[DefaultAppStorageEnvironmentKey.self]
|
||||
}
|
||||
|
@ -185,8 +188,8 @@ extension EnvironmentValues {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func defaultAppStorage(_ store: _StorageProvider) -> some View {
|
||||
public extension View {
|
||||
func defaultAppStorage(_ store: _StorageProvider) -> some View {
|
||||
environment(\._defaultAppStorage, store)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,23 @@ public protocol Scene {
|
|||
// FIXME: If I put `@SceneBuilder` in front of this
|
||||
// it fails to build with no useful error message.
|
||||
var body: Self.Body { get }
|
||||
|
||||
/// Override the default implementation for `Scene`s with body types of `Never`
|
||||
/// or in cases where the body would normally need to be type erased.
|
||||
///
|
||||
/// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor`
|
||||
func _visitChildren<V: SceneVisitor>(_ visitor: V)
|
||||
|
||||
/// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom
|
||||
/// `LayoutComputer` from the `SceneInputs`.
|
||||
///
|
||||
/// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`.
|
||||
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs
|
||||
}
|
||||
|
||||
public typealias SceneInputs<S: Scene> = ViewInputs<S>
|
||||
public typealias SceneOutputs = ViewOutputs
|
||||
|
||||
protocol TitledScene {
|
||||
var title: Text? { get }
|
||||
}
|
||||
|
|
|
@ -15,67 +15,280 @@
|
|||
// Created by Carson Katri on 7/16/20.
|
||||
//
|
||||
|
||||
@_functionBuilder
|
||||
public struct SceneBuilder {
|
||||
@resultBuilder
|
||||
public enum SceneBuilder {
|
||||
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
// swiftlint:disable large_tuple
|
||||
// swiftlint:disable function_parameter_count
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene, C1: Scene {
|
||||
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
||||
C1: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1),
|
||||
children: [_AnyScene(c0), _AnyScene(c1)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene where C0: Scene, C1: Scene, C2: Scene {
|
||||
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
||||
where C0: Scene, C1: Scene, C2: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||
_TupleScene((c0, c1, c2, c3), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4, c5), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4, C5>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4,
|
||||
_ c5: C5
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene,
|
||||
C5: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4, c5),
|
||||
children: [
|
||||
_AnyScene(c0),
|
||||
_AnyScene(c1),
|
||||
_AnyScene(c2),
|
||||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4, c5, c6), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4,
|
||||
_ c5: C5,
|
||||
_ c6: C6
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene,
|
||||
C4: Scene, C5: Scene, C6: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4, c5, c6),
|
||||
children: [
|
||||
_AnyScene(c0),
|
||||
_AnyScene(c1),
|
||||
_AnyScene(c2),
|
||||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
_AnyScene(c6),
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4,
|
||||
_ c5: C5,
|
||||
_ c6: C6,
|
||||
_ c7: C7
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
|
||||
C7: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4, c5, c6, c7),
|
||||
children: [
|
||||
_AnyScene(c0),
|
||||
_AnyScene(c1),
|
||||
_AnyScene(c2),
|
||||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
_AnyScene(c6),
|
||||
_AnyScene(c7),
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene, C8: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7, c8), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7), _AnyScene(c8)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4,
|
||||
_ c5: C5,
|
||||
_ c6: C6,
|
||||
_ c7: C7,
|
||||
_ c8: C8
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
|
||||
C7: Scene, C8: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4, c5, c6, c7, c8),
|
||||
children: [
|
||||
_AnyScene(c0),
|
||||
_AnyScene(c1),
|
||||
_AnyScene(c2),
|
||||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
_AnyScene(c6),
|
||||
_AnyScene(c7),
|
||||
_AnyScene(c8),
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
$0.visit(c8)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneBuilder {
|
||||
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene, C8: Scene, C9: Scene {
|
||||
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7), _AnyScene(c8), _AnyScene(c9)])
|
||||
public extension SceneBuilder {
|
||||
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(
|
||||
_ c0: C0,
|
||||
_ c1: C1,
|
||||
_ c2: C2,
|
||||
_ c3: C3,
|
||||
_ c4: C4,
|
||||
_ c5: C5,
|
||||
_ c6: C6,
|
||||
_ c7: C7,
|
||||
_ c8: C8,
|
||||
_ c9: C9
|
||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
|
||||
C7: Scene, C8: Scene, C9: Scene
|
||||
{
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9),
|
||||
children: [
|
||||
_AnyScene(c0),
|
||||
_AnyScene(c1),
|
||||
_AnyScene(c2),
|
||||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
_AnyScene(c6),
|
||||
_AnyScene(c7),
|
||||
_AnyScene(c8),
|
||||
_AnyScene(c9),
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
$0.visit(c8)
|
||||
$0.visit(c9)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ struct ScenePhaseKey: EnvironmentKey {
|
|||
static let defaultValue: ScenePhase = .active
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
public var scenePhase: ScenePhase {
|
||||
public extension EnvironmentValues {
|
||||
var scenePhase: ScenePhase {
|
||||
get {
|
||||
self[ScenePhaseKey.self]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,7 +15,7 @@
|
|||
// Created by Carson Katri on 7/17/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
/// The renderer must specify a default `_StorageProvider` before any `SceneStorage`
|
||||
/// values are accessed.
|
||||
|
@ -23,7 +23,8 @@ public enum _DefaultSceneStorageProvider {
|
|||
public static var `default`: _StorageProvider!
|
||||
}
|
||||
|
||||
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
|
||||
@propertyWrapper
|
||||
public struct SceneStorage<Value>: DynamicProperty {
|
||||
let key: String
|
||||
let defaultValue: Value
|
||||
let store: (_StorageProvider, String, Value) -> ()
|
||||
|
@ -53,42 +54,38 @@ public enum _DefaultSceneStorageProvider {
|
|||
|
||||
extension SceneStorage: ObservedProperty {}
|
||||
|
||||
extension SceneStorage {
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String) where Value == Bool {
|
||||
public extension SceneStorage {
|
||||
init(wrappedValue: Value, _ key: String) where Value == Bool {
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2) }
|
||||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String) where Value == Int {
|
||||
init(wrappedValue: Value, _ key: String) where Value == Int {
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2) }
|
||||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String) where Value == Double {
|
||||
init(wrappedValue: Value, _ key: String) where Value == Double {
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2) }
|
||||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String) where Value == String {
|
||||
init(wrappedValue: Value, _ key: String) where Value == String {
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2) }
|
||||
read = { $0.read(key: $1) }
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String)
|
||||
where Value: RawRepresentable, Value.RawValue == Int {
|
||||
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable,
|
||||
Value.RawValue == Int
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2.rawValue) }
|
||||
|
@ -100,9 +97,9 @@ extension SceneStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value,
|
||||
_ key: String)
|
||||
where Value: RawRepresentable, Value.RawValue == String {
|
||||
init(wrappedValue: Value, _ key: String)
|
||||
where Value: RawRepresentable, Value.RawValue == String
|
||||
{
|
||||
defaultValue = wrappedValue
|
||||
self.key = key
|
||||
store = { $0.store(key: $1, value: $2.rawValue) }
|
||||
|
|
|
@ -27,18 +27,17 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
self.content = content()
|
||||
}
|
||||
|
||||
@_disfavoredOverload public init(_ title: Text,
|
||||
id: String,
|
||||
@ViewBuilder content: () -> Content) {
|
||||
@_disfavoredOverload
|
||||
public init(_ title: Text, id: String, @ViewBuilder content: () -> Content) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@_disfavoredOverload public init<S>(_ title: S,
|
||||
id: String,
|
||||
@ViewBuilder content: () -> Content)
|
||||
where S: StringProtocol {
|
||||
@_disfavoredOverload
|
||||
public init<S>(_ title: S, id: String, @ViewBuilder content: () -> Content)
|
||||
where S: StringProtocol
|
||||
{
|
||||
self.id = id
|
||||
self.title = Text(title)
|
||||
self.content = content()
|
||||
|
@ -50,21 +49,21 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
self.content = content()
|
||||
}
|
||||
|
||||
@_disfavoredOverload public init(_ title: Text,
|
||||
@ViewBuilder content: () -> Content) {
|
||||
@_disfavoredOverload
|
||||
public init(_ title: Text, @ViewBuilder content: () -> Content) {
|
||||
id = ""
|
||||
self.title = title
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@_disfavoredOverload public init<S>(_ title: S,
|
||||
@ViewBuilder content: () -> Content)
|
||||
where S: StringProtocol {
|
||||
@_disfavoredOverload
|
||||
public init<S>(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol {
|
||||
id = ""
|
||||
self.title = Text(title)
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var body: Never {
|
||||
neverScene("WindowGroup")
|
||||
}
|
||||
|
@ -76,4 +75,8 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
// public init(_ titleKey: LocalizedStringKey,
|
||||
// @ViewBuilder content: () -> Content) {
|
||||
// }
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public struct _SceneModifier_Content<Modifier>: Scene where Modifier: _SceneModi
|
|||
public let modifier: Modifier
|
||||
public let scene: _AnyScene
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var body: Never {
|
||||
neverScene("_SceneModifier_Content")
|
||||
}
|
||||
|
@ -36,8 +37,8 @@ public extension Scene {
|
|||
}
|
||||
}
|
||||
|
||||
extension _SceneModifier where Body == Never {
|
||||
public func body(content: SceneContent) -> Body {
|
||||
public extension _SceneModifier where Body == Never {
|
||||
func body(content: SceneContent) -> Body {
|
||||
fatalError("""
|
||||
\(self) is a primitive `_SceneModifier`, you're not supposed to run `body(content:)`
|
||||
""")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,7 +15,7 @@
|
|||
// Created by Carson Katri on 7/19/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
public struct _AnyApp: App {
|
||||
var app: Any
|
||||
|
@ -23,7 +23,7 @@ public struct _AnyApp: App {
|
|||
let bodyClosure: (Any) -> _AnyScene
|
||||
let bodyType: Any.Type
|
||||
|
||||
init<A: App>(_ app: A) {
|
||||
public init<A: App>(_ app: A) {
|
||||
self.app = app
|
||||
type = A.self
|
||||
// swiftlint:disable:next force_cast
|
||||
|
@ -31,24 +31,37 @@ public struct _AnyApp: App {
|
|||
bodyType = A.Body.self
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var body: Never {
|
||||
neverScene("_AnyApp")
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public init() {
|
||||
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.")
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public static func _setTitle(_ title: String) {
|
||||
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||
}
|
||||
|
||||
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
|
||||
public static var _configuration: _AppConfiguration {
|
||||
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
|
||||
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,8 +15,6 @@
|
|||
// Created by Carson Katri on 7/19/20.
|
||||
//
|
||||
|
||||
import Runtime
|
||||
|
||||
public struct _AnyScene: Scene {
|
||||
/** The result type of `bodyClosure` allowing to disambiguate between scenes that
|
||||
produce other scenes or scenes that only produce containing views.
|
||||
|
@ -63,16 +61,12 @@ public struct _AnyScene: Scene {
|
|||
// swiftlint:disable:next force_cast
|
||||
bodyClosure = { .scene(_AnyScene(($0 as! S).body)) }
|
||||
}
|
||||
// FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get
|
||||
// a name of a type constructor in runtime. Should definitely check if these are different
|
||||
// across modules, otherwise can cause problems with scenes with same names in different
|
||||
// modules.
|
||||
|
||||
// swiftlint:disable:next force_try
|
||||
typeConstructorName = try! typeInfo(of: type).mangledName
|
||||
typeConstructorName = TokamakCore.typeConstructorName(type)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var body: Never {
|
||||
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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,7 +15,7 @@
|
|||
// Created by Carson Katri on 7/22/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
public protocol _StorageProvider {
|
||||
func store(key: String, value: Bool?)
|
||||
|
|
|
@ -17,11 +17,17 @@
|
|||
|
||||
struct _TupleScene<T>: Scene, GroupScene {
|
||||
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.children = children
|
||||
self.visit = visit
|
||||
}
|
||||
|
||||
var body: Never {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,43 +15,10 @@
|
|||
// Created by Carson Katri on 7/17/20.
|
||||
//
|
||||
|
||||
import Runtime
|
||||
|
||||
public protocol DynamicProperty {
|
||||
mutating func update()
|
||||
}
|
||||
|
||||
extension DynamicProperty {
|
||||
public mutating func update() {}
|
||||
}
|
||||
|
||||
extension TypeInfo {
|
||||
/// Extract all `DynamicProperty` from a type, recursively.
|
||||
/// This is necessary as a `DynamicProperty` can be nested.
|
||||
/// `EnvironmentValues` can also be injected at this point.
|
||||
func dynamicProperties(_ environment: EnvironmentValues,
|
||||
source: inout Any,
|
||||
shouldUpdate: Bool) -> [PropertyInfo] {
|
||||
var dynamicProps = [PropertyInfo]()
|
||||
for prop in properties where prop.type is DynamicProperty.Type {
|
||||
dynamicProps.append(prop)
|
||||
// swiftlint:disable force_try
|
||||
let propInfo = try! typeInfo(of: prop.type)
|
||||
propInfo.injectEnvironment(from: environment, into: &source)
|
||||
var extracted = try! prop.get(from: source)
|
||||
dynamicProps.append(
|
||||
contentsOf: propInfo.dynamicProperties(environment,
|
||||
source: &extracted,
|
||||
shouldUpdate: shouldUpdate)
|
||||
)
|
||||
// swiftlint:disable:next force_cast
|
||||
var extractedDynamicProp = extracted as! DynamicProperty
|
||||
if shouldUpdate {
|
||||
extractedDynamicProp.update()
|
||||
}
|
||||
try! prop.set(value: extractedDynamicProp, on: &source)
|
||||
// swiftlint:enable force_try
|
||||
}
|
||||
return dynamicProps
|
||||
}
|
||||
public extension DynamicProperty {
|
||||
mutating func update() {}
|
||||
}
|
||||
|
|
|
@ -23,14 +23,15 @@ protocol EnvironmentReader {
|
|||
mutating func setContent(from values: EnvironmentValues)
|
||||
}
|
||||
|
||||
@propertyWrapper public struct Environment<Value>: DynamicProperty {
|
||||
@propertyWrapper
|
||||
public struct Environment<Value>: DynamicProperty {
|
||||
enum Content {
|
||||
case keyPath(KeyPath<EnvironmentValues, Value>)
|
||||
case value(Value)
|
||||
}
|
||||
|
||||
var content: Content
|
||||
let keyPath: KeyPath<EnvironmentValues, Value>
|
||||
private var content: Content
|
||||
private let keyPath: KeyPath<EnvironmentValues, Value>
|
||||
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
|
||||
content = .keyPath(keyPath)
|
||||
self.keyPath = keyPath
|
||||
|
|
|
@ -17,11 +17,24 @@ public protocol EnvironmentKey {
|
|||
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)
|
||||
}
|
||||
|
||||
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 value: Value
|
||||
|
||||
|
@ -30,17 +43,15 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo
|
|||
self.value = value
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
public typealias Body = Never
|
||||
|
||||
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
public func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
values[keyPath: keyPath] = value
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func environment<V>(
|
||||
public extension View {
|
||||
func environment<V>(
|
||||
_ keyPath: WritableKeyPath<EnvironmentValues, V>,
|
||||
_ value: V
|
||||
) -> some View {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,11 +15,14 @@
|
|||
// Created by Carson Katri on 7/7/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
where ObjectType: ObservableObject {
|
||||
@dynamicMemberLookup public struct Wrapper {
|
||||
@propertyWrapper
|
||||
public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
where ObjectType: ObservableObject
|
||||
{
|
||||
@dynamicMemberLookup
|
||||
public struct Wrapper {
|
||||
internal let root: ObjectType
|
||||
public subscript<Subject>(
|
||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||
|
@ -70,8 +73,8 @@ extension ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func environmentObject<B>(_ bindable: B) -> some View where B: ObservableObject {
|
||||
public extension View {
|
||||
func environmentObject<B>(_ bindable: B) -> some View where B: ObservableObject {
|
||||
environment(B.environmentStore, bindable)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -12,11 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
public struct EnvironmentValues: CustomStringConvertible {
|
||||
public var description: String {
|
||||
String(describing: values)
|
||||
"EnvironmentValues: \(values.count)"
|
||||
}
|
||||
|
||||
private var values: [ObjectIdentifier: Any] = [:]
|
||||
|
@ -43,9 +43,40 @@ public struct EnvironmentValues: CustomStringConvertible {
|
|||
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 _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
||||
struct IsEnabledKey: EnvironmentKey {
|
||||
static let defaultValue = true
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var isEnabled: Bool {
|
||||
get {
|
||||
self[IsEnabledKey.self]
|
||||
}
|
||||
set {
|
||||
self[IsEnabledKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
|
||||
let environmentValues: EnvironmentValues
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
@ -57,8 +88,8 @@ struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func environmentValues(_ values: EnvironmentValues) -> some View {
|
||||
public extension View {
|
||||
func environmentValues(_ values: EnvironmentValues) -> some View {
|
||||
modifier(_EnvironmentValuesWritingModifier(environmentValues: values))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
protocol AppearanceActionProtocol {
|
||||
protocol AppearanceActionType {
|
||||
var appear: (() -> ())? { get }
|
||||
var disappear: (() -> ())? { get }
|
||||
}
|
||||
|
@ -21,26 +21,23 @@ protocol AppearanceActionProtocol {
|
|||
struct _AppearanceActionModifier: ViewModifier {
|
||||
var appear: (() -> ())?
|
||||
var disappear: (() -> ())?
|
||||
init(appear: (() -> ())? = nil, disappear: (() -> ())? = nil) {
|
||||
self.appear = appear
|
||||
self.disappear = disappear
|
||||
}
|
||||
|
||||
typealias Body = Never
|
||||
}
|
||||
|
||||
extension ModifiedContent: AppearanceActionProtocol
|
||||
where Content: View, Modifier == _AppearanceActionModifier {
|
||||
extension ModifiedContent: AppearanceActionType
|
||||
where Content: View, Modifier == _AppearanceActionModifier
|
||||
{
|
||||
var appear: (() -> ())? { modifier.appear }
|
||||
var disappear: (() -> ())? { modifier.disappear }
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func onAppear(perform action: (() -> ())? = nil) -> some View {
|
||||
public extension View {
|
||||
func onAppear(perform action: (() -> ())? = nil) -> some View {
|
||||
modifier(_AppearanceActionModifier(appear: action))
|
||||
}
|
||||
|
||||
public func onDisappear(perform action: (() -> ())? = nil) -> some View {
|
||||
func onDisappear(perform action: (() -> ())? = nil) -> some View {
|
||||
modifier(_AppearanceActionModifier(disappear: action))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +15,8 @@
|
|||
// Created by Carson Katri on 06/29/2020.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
|
||||
public var shape: ClipShape
|
||||
public var style: FillStyle
|
||||
|
@ -27,20 +29,29 @@ public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
|
|||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
|
||||
public var animatableData: ClipShape.AnimatableData {
|
||||
get { shape.animatableData }
|
||||
set { shape.animatableData = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func clipShape<S>(_ shape: S, style: FillStyle = FillStyle()) -> some View where S: Shape {
|
||||
public extension View {
|
||||
func clipShape<S>(_ shape: S, style: FillStyle = FillStyle()) -> some View where S: Shape {
|
||||
modifier(_ClipEffect(shape: shape, style: style))
|
||||
}
|
||||
|
||||
public func clipped(antialiased: Bool = false) -> some View {
|
||||
clipShape(Rectangle(),
|
||||
style: FillStyle(antialiased: antialiased))
|
||||
func clipped(antialiased: Bool = false) -> some View {
|
||||
clipShape(
|
||||
Rectangle(),
|
||||
style: FillStyle(antialiased: antialiased)
|
||||
)
|
||||
}
|
||||
|
||||
public func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View {
|
||||
clipShape(RoundedRectangle(cornerRadius: radius),
|
||||
style: FillStyle(antialiased: antialiased))
|
||||
func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View {
|
||||
clipShape(
|
||||
RoundedRectangle(cornerRadius: radius),
|
||||
style: FillStyle(antialiased: antialiased)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -15,8 +15,10 @@
|
|||
// Created by Carson Katri on 7/3/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// FIXME: Make `Animatable`
|
||||
public protocol GeometryEffect: ViewModifier {
|
||||
public protocol GeometryEffect: Animatable, ViewModifier {
|
||||
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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +15,8 @@
|
|||
// Created by Carson Katri on 7/3/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct _RotationEffect: GeometryEffect {
|
||||
public var angle: Angle
|
||||
public var anchor: UnitPoint
|
||||
|
@ -25,16 +27,25 @@ public struct _RotationEffect: GeometryEffect {
|
|||
}
|
||||
|
||||
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 {
|
||||
content
|
||||
}
|
||||
|
||||
public var animatableData: AnimatablePair<Angle.AnimatableData, UnitPoint.AnimatableData> {
|
||||
get {
|
||||
.init(angle.animatableData, anchor.animatableData)
|
||||
}
|
||||
set {
|
||||
(angle.animatableData, anchor.animatableData) = newValue[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
|
||||
public extension View {
|
||||
func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
|
||||
modifier(_RotationEffect(angle: angle, anchor: anchor))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
// 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
|
||||
// limitations under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct _FlexFrameLayout: ViewModifier {
|
||||
public let minWidth: CGFloat?
|
||||
public let idealWidth: CGFloat?
|
||||
|
@ -21,13 +23,25 @@ public struct _FlexFrameLayout: ViewModifier {
|
|||
public let maxHeight: CGFloat?
|
||||
public let alignment: Alignment
|
||||
|
||||
init(minWidth: CGFloat? = nil,
|
||||
idealWidth: CGFloat? = nil,
|
||||
maxWidth: CGFloat? = nil,
|
||||
minHeight: CGFloat? = nil,
|
||||
idealHeight: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil,
|
||||
alignment: Alignment) {
|
||||
// These are special cases in SwiftUI, where the child
|
||||
// will request the entire width/height of the parent.
|
||||
public var fillWidth: Bool {
|
||||
(minWidth == 0 || minWidth == nil) && maxWidth == .infinity
|
||||
}
|
||||
|
||||
public var fillHeight: Bool {
|
||||
(minHeight == 0 || minHeight == nil) && maxHeight == .infinity
|
||||
}
|
||||
|
||||
init(
|
||||
minWidth: CGFloat? = nil,
|
||||
idealWidth: CGFloat? = nil,
|
||||
maxWidth: CGFloat? = nil,
|
||||
minHeight: CGFloat? = nil,
|
||||
idealHeight: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil,
|
||||
alignment: Alignment
|
||||
) {
|
||||
self.minWidth = minWidth
|
||||
self.idealWidth = idealWidth
|
||||
self.maxWidth = maxWidth
|
||||
|
@ -42,14 +56,20 @@ public struct _FlexFrameLayout: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func frame(minWidth: CGFloat? = nil,
|
||||
idealWidth: CGFloat? = nil,
|
||||
maxWidth: CGFloat? = nil,
|
||||
minHeight: CGFloat? = nil,
|
||||
idealHeight: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil,
|
||||
alignment: Alignment = .center) -> some View {
|
||||
extension _FlexFrameLayout: Animatable {
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func frame(
|
||||
minWidth: CGFloat? = nil,
|
||||
idealWidth: CGFloat? = nil,
|
||||
maxWidth: CGFloat? = nil,
|
||||
minHeight: CGFloat? = nil,
|
||||
idealHeight: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil,
|
||||
alignment: Alignment = .center
|
||||
) -> some View {
|
||||
func areInNondecreasingOrder(
|
||||
_ min: CGFloat?, _ ideal: CGFloat?, _ max: CGFloat?
|
||||
) -> Bool {
|
||||
|
@ -59,8 +79,9 @@ extension View {
|
|||
return min <= ideal && ideal <= max
|
||||
}
|
||||
|
||||
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth)
|
||||
|| !areInNondecreasingOrder(minHeight, idealHeight, maxHeight) {
|
||||
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth) ||
|
||||
!areInNondecreasingOrder(minHeight, idealHeight, maxHeight)
|
||||
{
|
||||
fatalError("Contradictory frame constraints specified.")
|
||||
}
|
||||
|
||||
|
@ -71,6 +92,7 @@ extension View {
|
|||
minHeight: minHeight,
|
||||
idealHeight: idealHeight, maxHeight: maxHeight,
|
||||
alignment: alignment
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// 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.
|
||||
|
@ -12,14 +12,14 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct _FrameLayout: ViewModifier {
|
||||
public let width: CGFloat?
|
||||
public let height: CGFloat?
|
||||
public let alignment: Alignment
|
||||
|
||||
init(width: CGFloat?,
|
||||
height: CGFloat?,
|
||||
alignment: Alignment) {
|
||||
init(width: CGFloat?, height: CGFloat?, alignment: Alignment) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.alignment = alignment
|
||||
|
@ -30,10 +30,16 @@ public struct _FrameLayout: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
public func frame(width: CGFloat? = nil,
|
||||
height: CGFloat? = nil,
|
||||
alignment: Alignment = .center) -> some View {
|
||||
extension _FrameLayout: Animatable {
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func frame(
|
||||
width: CGFloat? = nil,
|
||||
height: CGFloat? = nil,
|
||||
alignment: Alignment = .center
|
||||
) -> some View {
|
||||
modifier(_FrameLayout(width: width, height: height, alignment: alignment))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// 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.
|
||||
|
||||
// FIXME: these should have standalone implementations
|
||||
public extension View {
|
||||
@_spi(TokamakCore)
|
||||
func _onMount(perform action: (() -> ())? = nil) -> some View {
|
||||
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 {
|
||||
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 }
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue