Compare commits
103 Commits
100-percen
...
main
Author | SHA1 | Date |
---|---|---|
|
eb50bca9f7 | |
|
cf1304ef90 | |
![]() |
e0d8e9db46 | |
![]() |
f1cbfcf073 | |
![]() |
56822c906b | |
![]() |
af810902bd | |
![]() |
b24b964fd7 | |
![]() |
e7e318e64e | |
![]() |
47c5a05068 | |
![]() |
fa0a8d2447 | |
![]() |
687baee97f | |
![]() |
10d9d32b97 | |
![]() |
4e8b84e4a1 | |
![]() |
676760d34b | |
![]() |
9d0e2fc067 | |
![]() |
d78ab20ea8 | |
![]() |
5e6fe2d2c9 | |
![]() |
c4717d5cae | |
![]() |
0b182d99a1 | |
![]() |
546b9e572f | |
![]() |
c935744ae8 | |
![]() |
6e2ccf71ea | |
![]() |
9db23c9e3f | |
![]() |
f4cd4955db | |
![]() |
03513dd5b3 | |
![]() |
355c880a1d | |
![]() |
c0c4534352 | |
![]() |
a604ef5269 | |
![]() |
3081f5521a | |
![]() |
2e7f561276 | |
![]() |
2ba548810c | |
![]() |
8177fc8cae | |
![]() |
a41ac37500 | |
![]() |
5b7e7058e8 | |
![]() |
337b80a4c4 | |
![]() |
528fe056e0 | |
![]() |
39d37a96c8 | |
![]() |
6905fdff19 | |
![]() |
fd64eafde8 | |
![]() |
eef6bb2da3 | |
![]() |
1952170ce8 | |
![]() |
3c649d5ead | |
![]() |
12606a809e | |
![]() |
a9addc8cb1 | |
![]() |
077c0cdfcb | |
![]() |
5c3bc9d783 | |
![]() |
d2d79c2bbf | |
![]() |
32616fe9d4 | |
![]() |
cbfdc34793 | |
![]() |
a5a05b4826 | |
![]() |
2ad85b329d | |
![]() |
f07b9fa883 | |
![]() |
dfc79e4148 | |
![]() |
005996262a | |
![]() |
da66063918 | |
![]() |
8788fd64e9 | |
![]() |
88cee68bee | |
![]() |
19bcf2746b | |
![]() |
3d9558f1b8 | |
![]() |
e1fcd180d7 | |
![]() |
bd1d8138c3 | |
![]() |
c2ed28ca40 | |
![]() |
4609b0a203 | |
![]() |
21c21cd328 | |
![]() |
a8c6eae94e | |
![]() |
9a568ab9cf | |
![]() |
ae0db4d1f1 | |
![]() |
22ea230ce0 | |
![]() |
6792dbb02c | |
![]() |
12a6256ec0 | |
![]() |
ab5e564ada | |
![]() |
a064956095 | |
![]() |
ff3f81dbfd | |
![]() |
b6790c5c6d | |
![]() |
30f55d9814 | |
![]() |
2efa80a57d | |
![]() |
4a7748ad6b | |
![]() |
54146b8a38 | |
![]() |
79a9a66da2 | |
![]() |
738455be68 | |
![]() |
719c109811 | |
![]() |
2dcbc67cd3 | |
![]() |
9aa88a1978 | |
![]() |
d35e37c4f5 | |
![]() |
e6c37a4c80 | |
![]() |
ae219e947b | |
![]() |
ac69bbc3e5 | |
![]() |
3302a5163c | |
![]() |
5926e9f182 | |
![]() |
da9843d07f | |
![]() |
44280847cf | |
![]() |
77759777f1 | |
![]() |
096ec5c4a2 | |
![]() |
57f57174c5 | |
![]() |
4a372c3fa8 | |
![]() |
d914e1bdc9 | |
![]() |
5c458f92b8 | |
![]() |
bde7de9be0 | |
![]() |
8076035120 | |
![]() |
4d211af563 | |
![]() |
4a1101c21a | |
![]() |
19cf8b6782 | |
![]() |
eeddfe9e4b |
|
@ -3,7 +3,7 @@ name: Bug report
|
|||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: MaxDesiatov
|
||||
assignees: carson-katri
|
||||
|
||||
---
|
||||
|
||||
|
@ -28,17 +28,17 @@ A clear and concise description of what you expected to happen.
|
|||
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. macOS]
|
||||
- OS: [e.g. macOS 12.4]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version of the browser [e.g. 22]
|
||||
- Version of Tokamak [e.g. 0.6.1]
|
||||
- Version of Tokamak [e.g. 0.10.1]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- 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.6.1]
|
||||
- Version of Tokamak [e.g. 0.10.1]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
|
|
@ -6,17 +6,39 @@ on:
|
|||
branches: [main]
|
||||
|
||||
jobs:
|
||||
swiftwasm_build:
|
||||
swiftwasm_bundle_5_6:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: swiftwasm/swiftwasm-action@v5.3
|
||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||
with:
|
||||
shell-action: carton bundle --product TokamakDemo
|
||||
- name: Check binary size
|
||||
shell: bash
|
||||
run: |
|
||||
ls -la Bundle
|
||||
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
|
||||
|
||||
swiftwasm_test:
|
||||
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-11.0
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -24,16 +46,17 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo xcode-select --switch /Applications/Xcode_12.3.app/Contents/Developer/
|
||||
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
|
||||
# avoid building unrelated products for testing by specifying the test product explicitly
|
||||
swift build --product TokamakPackageTests
|
||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
||||
|
||||
rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||
|
||||
xcodebuild -version
|
||||
|
||||
# make sure Tokamak can be built on macOS so that Xcode autocomplete works
|
||||
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||
xcpretty --color
|
||||
|
@ -46,23 +69,33 @@ jobs:
|
|||
|
||||
./benchmark.sh
|
||||
|
||||
gtk_macos_build:
|
||||
runs-on: macos-latest
|
||||
- name: Upload failed snapshots
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: Failed snapshots
|
||||
path: '*Tests'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the GTK renderer on macOS
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo xcode-select --switch /Applications/Xcode_12.3.app/Contents/Developer/
|
||||
|
||||
brew install gtk+3
|
||||
|
||||
make build
|
||||
# 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-18.04
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-bionic
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -70,13 +103,14 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk+-3.0 gtk+-3.0
|
||||
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-20.04
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -84,7 +118,6 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk+-3.0 gtk+-3.0
|
||||
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
|
||||
|
||||
make build
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
name: Codecov
|
||||
|
||||
on:
|
||||
|
@ -9,27 +8,26 @@ on:
|
|||
jobs:
|
||||
codecov:
|
||||
container:
|
||||
image: swift:5.3.2-bionic
|
||||
image: swiftlang/swift:nightly-5.7-focal
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: apt-get update && apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Test Target
|
||||
run: swift build --enable-test-discovery -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||
- name: Run Tests
|
||||
run: swift test --enable-test-discovery --enable-code-coverage --skip-build
|
||||
- name: Generate Branch Coverage Report
|
||||
uses: mattpolzin/swift-codecov-action@0.6.1
|
||||
id: cov
|
||||
with:
|
||||
MINIMUM_COVERAGE: 20
|
||||
- name: Post Positive Results
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- name: Post Negative Results
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
|
||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Test Target
|
||||
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
|
||||
- name: Run Tests
|
||||
run: swift test --enable-code-coverage --skip-build
|
||||
- name: Generate Branch Coverage Report
|
||||
uses: mattpolzin/swift-codecov-action@0.7.1
|
||||
id: cov
|
||||
with:
|
||||
MINIMUM_COVERAGE: 15
|
||||
- name: Post Positive Results
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
- name: Post Negative Results
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
|
||||
|
|
|
@ -24,6 +24,7 @@ jobs:
|
|||
dependencies,
|
||||
documentation,
|
||||
enhancement,
|
||||
Fiber,
|
||||
refactor,
|
||||
SwiftUI compatibility,
|
||||
test suite,
|
||||
|
|
|
@ -12,7 +12,13 @@ jobs:
|
|||
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: norio-nomura/action-swiftlint@3.1.0
|
||||
uses: mayk-it/action-swiftlint@3.2.2
|
||||
env:
|
||||
DIFF_BASE: ${{ github.base_ref }}
|
||||
DIFF_HEAD: HEAD
|
||||
|
|
|
@ -41,3 +41,6 @@ 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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
wasm-5.6.0-RELEASE
|
|
@ -8,5 +8,7 @@
|
|||
--maxwidth 100
|
||||
--wraparguments before-first
|
||||
--funcattributes prev-line
|
||||
--typeattributes prev-line
|
||||
--varattributes prev-line
|
||||
--disable andOperator
|
||||
--swiftversion 5.3
|
||||
--swiftversion 5.6
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"preLaunchTask": "make",
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/.build/debug/TokamakGTKDemo",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
192
CHANGELOG.md
192
CHANGELOG.md
|
@ -1,3 +1,195 @@
|
|||
# 0.10.1 (20 May 2022)
|
||||
|
||||
This is a small bugfix release, which updates JavaScriptKit dependency to 0.15 and required version of `carton` to 0.15.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Update JSKit dependency ([#482](https://github.com/TokamakUI/Tokamak/pull/482)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Explicitly mention `carton` version in "Requirements" ([#481](https://github.com/TokamakUI/Tokamak/pull/481)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use stable `v5.6` version of `swiftwasm-action` ([#477](https://github.com/TokamakUI/Tokamak/pull/477)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.10.0 (9 April 2022)
|
||||
|
||||
This release adds support for SwiftWasm 5.6. It also updates JavaScriptKit and OpenCombineJS dependencies.
|
||||
Due to issues with support for older SwiftWasm releases in the `carton`/SwiftPM integration, Tokamak now requires
|
||||
SwiftWasm 5.6 or later, while SwiftWasm 5.4 and 5.5 are no longer supported.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Build and test with SwiftWasm 5.6 on CI ([#475](https://github.com/TokamakUI/Tokamak/pull/475)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.9.1 (16 February 2022)
|
||||
|
||||
This release fixes an issue with `EnvironmentValues`, updates CI workflow for SwiftWasm 5.5, and bumps JavaScriptKit dependency to 0.12.0.
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix typo ([#462](https://github.com/TokamakUI/Tokamak/pull/462)) via [@regexident](https://github.com/regexident)
|
||||
- Fix `rootEnvironment` not merged with `.defaultEnvironment` ([#461](https://github.com/TokamakUI/Tokamak/pull/461)) via [@regexident](https://github.com/regexident)
|
||||
- Build and test with SwiftWasm 5.5 on CI ([#460](https://github.com/TokamakUI/Tokamak/pull/460)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.9.0 (26 November 2021)
|
||||
|
||||
This release adds support for SwiftWasm 5.5 and bumps the minimum required version to Swift 5.4.
|
||||
It now depends on JavaScriptKit 0.11.1, which no longer requires manual memory management of
|
||||
`JSClosure` instances. The downside of that update is that minimum browser version requirements are
|
||||
significantly higher now. See [`README.md`](README.md#requirements) for more details.
|
||||
|
||||
Additionally, a few new features were added to the DOM renderer:
|
||||
|
||||
- `Canvas` and `TimelineView`;
|
||||
- `onHover` modifier;
|
||||
- `task` modifier for running `async` functions;
|
||||
- Sanitizers for `Text` view.
|
||||
|
||||
Many thanks (in alphabetical order) to [@agg23](https://github.com/agg23),
|
||||
[@carson-katri](https://github.com/carson-katri), [@ezraberch](https://github.com/ezraberch),
|
||||
and [@mbrandonw](https://github.com/mbrandonw) for their contributions to this release!
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- `TextField` Not Rendering the field ([#455](https://github.com/TokamakUI/Tokamak/issues/455))
|
||||
- Can't find `CGSize` or `CGFloat` type ([#450](https://github.com/TokamakUI/Tokamak/issues/450))
|
||||
- `UnitPoint` constants don't match SwiftUI ([#443](https://github.com/TokamakUI/Tokamak/issues/443))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Update for JSKit 0.11.1, add async `task` modifier ([#457](https://github.com/TokamakUI/Tokamak/pull/457)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Switch to Xcode 13.0 in `gtk_macos_build` job ([#454](https://github.com/TokamakUI/Tokamak/pull/454)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `Canvas` and `TimelineView` to DOM renderer ([#449](https://github.com/TokamakUI/Tokamak/pull/449)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Initial implementation of `onHover` ([#448](https://github.com/TokamakUI/Tokamak/pull/448)) via [@agg23](https://github.com/agg23)
|
||||
- Refactor `NavigationView` ([#446](https://github.com/TokamakUI/Tokamak/pull/446)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Save HTML snapshots with .html extension. ([#447](https://github.com/TokamakUI/Tokamak/pull/447)) via [@mbrandonw](https://github.com/mbrandonw)
|
||||
- Add HTML renderer support for AngularGradient ([#444](https://github.com/TokamakUI/Tokamak/pull/444)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Bump requirements to Swift 5.4, migrate to `@resultBuilder` ([#442](https://github.com/TokamakUI/Tokamak/pull/442)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add HTML sanitizer to `Text` ([#437](https://github.com/TokamakUI/Tokamak/pull/437)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Add `@ezraberch` to the list of maintainers ([#440](https://github.com/TokamakUI/Tokamak/pull/440)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.8.0 (17 August 2021)
|
||||
|
||||
This release adds support for more SwiftUI types and modifiers, and fixes bugs. Including, but not
|
||||
limited to:
|
||||
|
||||
- `Toolbar` type and `toolbar` modifier
|
||||
- `ProgressView` type
|
||||
- `Animation` and related types and modifiers
|
||||
- `opacity`, `scaleEffect`, `aspectRatio`, and `controlSize` modifiers
|
||||
- `Material` and `Gradient` types
|
||||
- `HierarchicalShapeStyle` (`.primary`/`.secondary`/`.tertiary`/`.quaternary`) type
|
||||
- `ContainerRelativeShape` type
|
||||
- `spacing` argument support for initializers of `HStack` and `VStack`
|
||||
- support for standard Foundation types, such as `CGRect`, `CGSize` (we previously used our own
|
||||
implementation of those, which weren't fully compatible with Foundation)
|
||||
- ability to sort HTML attributes when generating static HTML, which is essential for end-to-end
|
||||
tests that cover generated output.
|
||||
|
||||
Many thanks to [@carson-katri](https://github.com/carson-katri),
|
||||
[@ezraberch](https://github.com/ezraberch), and [@yonihemi](https://github.com/yonihemi) for
|
||||
their contributions to this release!
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Is there anyway to compile this from Xcode? ([#406](https://github.com/TokamakUI/Tokamak/issues/406))
|
||||
- Xcode doesn't compile — gtk/gtk.h not found ([#405](https://github.com/TokamakUI/Tokamak/issues/405))
|
||||
- Use `NSGeometry` types from Foundation ([#404](https://github.com/TokamakUI/Tokamak/issues/404))
|
||||
- Adding padding to a view contained in a Button causes the Button to disappear ([#403](https://github.com/TokamakUI/Tokamak/issues/403))
|
||||
- .background modifier with contained shape causes view to expand to full vertical size of the screen ([#402](https://github.com/TokamakUI/Tokamak/issues/402))
|
||||
- Multi-line string handling in Text views ([#400](https://github.com/TokamakUI/Tokamak/issues/400))
|
||||
- Content with spacer jumps when blurring and focusing the page ([#395](https://github.com/TokamakUI/Tokamak/issues/395))
|
||||
- Frame sizes do not match expected behavior. ([#387](https://github.com/TokamakUI/Tokamak/issues/387))
|
||||
- URL hash change demo crashes ([#369](https://github.com/TokamakUI/Tokamak/issues/369))
|
||||
- Infinite loops w/ 100% CPU usage caused by stack overflows ([#367](https://github.com/TokamakUI/Tokamak/issues/367))
|
||||
- TokamakDemo breaks after use of `_domRef` ([#326](https://github.com/TokamakUI/Tokamak/issues/326))
|
||||
- Add support for `toolbar` modifier and related types ([#316](https://github.com/TokamakUI/Tokamak/issues/316))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Revise `ShapeStyle` and add `Gradient`s ([#435](https://github.com/TokamakUI/Tokamak/pull/435)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `Toolbar` implementation for HTML renderer ([#169](https://github.com/TokamakUI/Tokamak/pull/169)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Fix SwiftLint action ([#434](https://github.com/TokamakUI/Tokamak/pull/434)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Add View Traits and transitions ([#426](https://github.com/TokamakUI/Tokamak/pull/426)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `ToolbarItem` and its builder functions ([#430](https://github.com/TokamakUI/Tokamak/pull/430)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `controlSize`/`controlProminence` modifiers ([#431](https://github.com/TokamakUI/Tokamak/pull/431)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Fix background/overlay layout in DOM/HTML renderers ([#429](https://github.com/TokamakUI/Tokamak/pull/429)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `ProgressView` ([#425](https://github.com/TokamakUI/Tokamak/pull/425)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add support for custom fonts ([#421](https://github.com/TokamakUI/Tokamak/pull/421)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Animation implementation using the Web Animations API ([#427](https://github.com/TokamakUI/Tokamak/pull/427)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `scaleEffect` modifier ([#424](https://github.com/TokamakUI/Tokamak/pull/424)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `aspectRatio` modifier ([#422](https://github.com/TokamakUI/Tokamak/pull/422)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Check minWidth/Height == nil ([#420](https://github.com/TokamakUI/Tokamak/pull/420)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add Primary/Secondary/Tertiary/QuaternaryContentStyle ([#419](https://github.com/TokamakUI/Tokamak/pull/419)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `Material` to the HTML renderer ([#418](https://github.com/TokamakUI/Tokamak/pull/418)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Improve ShapeStyles to match iOS 15+ ([#417](https://github.com/TokamakUI/Tokamak/pull/417)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add ContainerRelativeShape ([#416](https://github.com/TokamakUI/Tokamak/pull/416)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add HTML implementation for `opacity` modifier ([#415](https://github.com/TokamakUI/Tokamak/pull/415)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Support `spacing` property on `HStack`/`VStack` ([#273](https://github.com/TokamakUI/Tokamak/pull/273)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Explicitly import CoreFoundation ([#413](https://github.com/TokamakUI/Tokamak/pull/413)) via [@yonihemi](https://github.com/yonihemi)
|
||||
- Fix handling of stroked shapes ([#414](https://github.com/TokamakUI/Tokamak/pull/414)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Add a snapshot test for `Path` SVG layout ([#412](https://github.com/TokamakUI/Tokamak/pull/412)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Attempt `padding` modifier fusion to avoid nested `div`s ([#253](https://github.com/TokamakUI/Tokamak/pull/253)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use `CGFloat`, `CGPoint`, `CGRect` from Foundation ([#411](https://github.com/TokamakUI/Tokamak/pull/411)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add reconciler stress tests for elaborate testing ([#381](https://github.com/TokamakUI/Tokamak/pull/381)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix spacers after `DOMRenderer.update` ([#410](https://github.com/TokamakUI/Tokamak/pull/410)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Replace `ViewDeferredToRenderer`, fix renderer tests ([#408](https://github.com/TokamakUI/Tokamak/pull/408)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Allow DOMRenderer to render buttons with non-Text labels (#403) ([#409](https://github.com/TokamakUI/Tokamak/pull/409)) via [@ezraberch](https://github.com/ezraberch)
|
||||
- Sort attributes in HTML nodes when rendering ([#346](https://github.com/TokamakUI/Tokamak/pull/346)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.7.0 (3 May 2021)
|
||||
|
||||
This release introduces new view types such as `DatePicker`, new modifiers such as `shadow`,
|
||||
improves test coverage, updates dependencies, and fixes multiple bugs and crashes. Additionally,
|
||||
a proof of concept GTK renderer is now available in the `TokamakGTK` module.
|
||||
|
||||
Many thanks to (in alphabetical order)
|
||||
[@carson-katri](https://github.com/carson-katri), [@filip-sakel](https://github.com/filip-sakel),
|
||||
[@foscomputerservices](https://github.com/foscomputerservices), [@literalpie](https://github.com/literalpie),
|
||||
[@mattpolzin](https://github.com/mattpolzin), [@mortenbekditlevsen](https://github.com/mortenbekditlevsen),
|
||||
and [@Snowy1803](https://github.com/Snowy1803) for their contributions to this release!
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- `@ObservedObject` is a get-only property ([#392](https://github.com/TokamakUI/Tokamak/issues/392))
|
||||
- What is the difference between `HTML` and `DynamicHTML`? ([#388](https://github.com/TokamakUI/Tokamak/issues/388))
|
||||
- Reduce `View.body` Visibility ([#385](https://github.com/TokamakUI/Tokamak/issues/385))
|
||||
- Verify that type constructor names contain contain module names ([#368](https://github.com/TokamakUI/Tokamak/issues/368))
|
||||
- Crash when using a `View` with optional content ([#362](https://github.com/TokamakUI/Tokamak/issues/362))
|
||||
- Set up code coverage reports on GitHub Actions ([#350](https://github.com/TokamakUI/Tokamak/issues/350))
|
||||
- Shadow support ([#324](https://github.com/TokamakUI/Tokamak/issues/324))
|
||||
- Implement `DatePicker` view in the DOM renderer ([#320](https://github.com/TokamakUI/Tokamak/issues/320))
|
||||
- `TokamakDemo` build failed ([#305](https://github.com/TokamakUI/Tokamak/issues/305))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add `@dynamicMemberLookup` to `Binding` ([#396](https://github.com/TokamakUI/Tokamak/pull/396)) via [@carson-katri](https://github.com/carson-katri)
|
||||
- Add `DatePicker` to the `TokamakDOM` module ([#394](https://github.com/TokamakUI/Tokamak/pull/394)) via [@Snowy1803](https://github.com/Snowy1803)
|
||||
- Use `String(reflecting:)` vs `String(describing:)` ([#391](https://github.com/TokamakUI/Tokamak/pull/391)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Clarify the difference between `HTML` and `DynamicHTML` ([#389](https://github.com/TokamakUI/Tokamak/pull/389)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add `_spi(TokamakCore)` to ideally internal public members ([#386](https://github.com/TokamakUI/Tokamak/pull/386)) via [@filip-sakel](https://github.com/filip-sakel)
|
||||
- Make properties of `CGPoint`, `CGSize` and `CGRect` `var`s instead of `let`s ([#382](https://github.com/TokamakUI/Tokamak/pull/382)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||
- Use immediate scheduler in `TestRenderer` ([#380](https://github.com/TokamakUI/Tokamak/pull/380)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Simple Code Coverage analysis ([#378](https://github.com/TokamakUI/Tokamak/pull/378)) via [@mattpolzin](https://github.com/mattpolzin)
|
||||
- Add checks for metadata state ([#375](https://github.com/TokamakUI/Tokamak/pull/375)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use upstream OpenCombine instead of a fork ([#377](https://github.com/TokamakUI/Tokamak/pull/377)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Update JavaScriptKit, OpenCombineJS dependencies ([#376](https://github.com/TokamakUI/Tokamak/pull/376)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Clean up metadata reflection code ([#372](https://github.com/TokamakUI/Tokamak/pull/372)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add David Hunt to the list of maintainers ([#373](https://github.com/TokamakUI/Tokamak/pull/373)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Refactor environment injection, add a test ([#371](https://github.com/TokamakUI/Tokamak/pull/371)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Replace uses of the Runtime library with stdlib ([#370](https://github.com/TokamakUI/Tokamak/pull/370)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Use `macos-latest` agent for the GTK build ([#360](https://github.com/TokamakUI/Tokamak/pull/360)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add a benchmark target and a script to run it ([#365](https://github.com/TokamakUI/Tokamak/pull/365)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Fix crashes in views with optional content ([#364](https://github.com/TokamakUI/Tokamak/pull/364)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add GTK support for `SecureField` ([#363](https://github.com/TokamakUI/Tokamak/pull/363)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||
- Add support for shadow modifier ([#355](https://github.com/TokamakUI/Tokamak/pull/355)) via [@literalpie](https://github.com/literalpie)
|
||||
- Two infinite loop fixes ([#359](https://github.com/TokamakUI/Tokamak/pull/359)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||
- Added `TextField` support for GTK using `GtkEntry` ([#361](https://github.com/TokamakUI/Tokamak/pull/361)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||
- Fixed a small issue with re-renderers being dropped ([#356](https://github.com/TokamakUI/Tokamak/pull/356)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||
- Removed an extra space that cause Safari to issue "Invalid value" ([#358](https://github.com/TokamakUI/Tokamak/pull/358)) via [@foscomputerservices](https://github.com/foscomputerservices)
|
||||
- Add `@mortenbekditlevsen` to the list of maintainers in `README.md` ([#352](https://github.com/TokamakUI/Tokamak/pull/352)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Build the GTK renderer on Ubuntu on CI ([#347](https://github.com/TokamakUI/Tokamak/pull/347)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- Add missing `Link` re-export to TokamakDOM ([#351](https://github.com/TokamakUI/Tokamak/pull/351)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
- GTK shape support WIP ([#348](https://github.com/TokamakUI/Tokamak/pull/348)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
|
||||
- Add a "bug report" issue template ([#349](https://github.com/TokamakUI/Tokamak/pull/349)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
|
||||
|
||||
# 0.6.1 (6 December 2020)
|
||||
|
||||
This release fixes autocomplete in Xcode for projects that depend on Tokamak.
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
### Modular structure
|
||||
|
||||
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
|
||||
separate modules for platform-specific renderers. Currently, the only available renderer modules are
|
||||
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
|
||||
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers
|
||||
guide](docs/RenderersGuide.md) for more details.
|
||||
|
||||
Tokamak users only need to import a renderer module they would like to use, while
|
||||
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
|
||||
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
|
||||
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
|
||||
those symbols with underscores in their names to indicate this. It can be formulated as these
|
||||
"rules":
|
||||
|
||||
1. If a symbol is restricted to a module and has no `public` access control, no need for an
|
||||
underscore.
|
||||
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
|
||||
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
|
||||
renderer module via `public typealias`.
|
||||
3. If a function or a type have `public` on them only by necessity to make them available in
|
||||
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
|
||||
indicate that.
|
||||
|
||||
The benefit of separate modules is that they allow us to provide separate renderers for different
|
||||
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
|
||||
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
|
||||
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
|
||||
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
|
||||
`TokamakAndroid` with no need to be aware of any of the web modules.
|
||||
|
||||
### Testing
|
||||
|
||||
Tokamak uses [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library to
|
||||
make sure that HTML and layout generated by renderers stay consistent. To run the test suite on macOS
|
||||
you should use this command:
|
||||
|
||||
```sh
|
||||
swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
|
||||
```
|
||||
|
||||
Unfortunately, plain `swift test` won't work as it tries to build targets that aren't related to
|
||||
the test suite.
|
||||
|
||||
### Coding Style
|
||||
|
||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
||||
[SwiftLint](https://github.com/realm/SwiftLint) to enforce formatting and coding style. SwiftFormat
|
||||
0.45.3 and SwiftLint 0.39.2 or later versions are recommended. We encourage you to run SwiftFormat
|
||||
and SwiftLint within a local clone of the repository in whatever way works best for you. You can do
|
||||
that either manually, or automatically with VSCode extensions for
|
||||
[SwiftFormat](https://github.com/vknabel/vscode-swiftformat) and
|
||||
[SwiftLint](https://github.com/vknabel/vscode-swiftlint) respectively, or with the [Xcode
|
||||
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension), or [build
|
||||
phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase).
|
||||
|
||||
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run
|
||||
this once to set up the [pre-commit](https://pre-commit.com/) hook:
|
||||
|
||||
```
|
||||
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
|
||||
pre-commit install # installs pre-commit hook to run checks before you commit
|
||||
```
|
||||
|
||||
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
|
||||
and installation instructions for other platforms.
|
||||
|
||||
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
|
||||
fail with inconsistent formatting or style. We require CI builds to pass for all
|
||||
PRs before merging.
|
2
LICENSE
2
LICENSE
|
@ -186,7 +186,7 @@ file or class name and description of purpose be included on the
|
|||
same "printed page" as the copyright notice for easier
|
||||
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.
|
||||
|
|
|
@ -7,59 +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 */; };
|
||||
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
|
||||
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
|
||||
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
|
||||
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
|
||||
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
|
||||
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
|
||||
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; };
|
||||
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 */; };
|
||||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
||||
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
||||
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
|
||||
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; };
|
||||
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; };
|
||||
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.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 */; };
|
||||
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
|
||||
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
|
||||
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
|
||||
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.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 */
|
||||
|
@ -92,40 +108,48 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
|
||||
4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
|
||||
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
|
||||
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
|
||||
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
|
||||
85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
|
||||
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
|
||||
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.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>"; };
|
||||
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
|
||||
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.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 */
|
||||
|
@ -185,34 +209,106 @@
|
|||
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
|
||||
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */,
|
||||
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
|
||||
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 */,
|
||||
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */,
|
||||
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
|
||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
|
||||
8500293E24D2FF3E001A2E84 /* SliderDemo.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 */,
|
||||
4550BD5125B642B80088F4EA /* ShadowDemo.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 = (
|
||||
|
@ -351,30 +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 */,
|
||||
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
|
||||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
||||
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
|
||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
||||
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
|
||||
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
||||
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
||||
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */,
|
||||
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 */,
|
||||
D1C726F324CB63C6003B576D /* ButtonStyleDemo.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;
|
||||
};
|
||||
|
@ -382,30 +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 */,
|
||||
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
|
||||
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
||||
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
|
||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
||||
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
|
||||
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
||||
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
||||
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */,
|
||||
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 */,
|
||||
D1C726F424CB63C6003B576D /* ButtonStyleDemo.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;
|
||||
};
|
||||
|
|
105
Package.resolved
105
Package.resolved
|
@ -1,52 +1,59 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "JavaScriptKit",
|
||||
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "ebd9ca04215397f0e3cb72d6e96406a980a424e5",
|
||||
"version": "0.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombine",
|
||||
"repositoryURL": "https://github.com/OpenCombine/OpenCombine.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "28993ae57de5a4ea7e164787636cafad442d568c",
|
||||
"version": "0.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombineJS",
|
||||
"repositoryURL": "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "eaf324ce78710f53b52fb82e9a8de4693633e33a",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-argument-parser",
|
||||
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
|
||||
"version": "0.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Benchmark",
|
||||
"repositoryURL": "https://github.com/google/swift-benchmark",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8e0ef8bb7482ab97dcd2cd1d6855bd38921c345d",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
"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
|
||||
}
|
||||
|
|
133
Package.swift
133
Package.swift
|
@ -1,6 +1,4 @@
|
|||
// 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
|
||||
|
||||
|
@ -26,8 +24,8 @@ let package = Package(
|
|||
targets: ["TokamakStaticHTML"]
|
||||
),
|
||||
.executable(
|
||||
name: "TokamakStaticDemo",
|
||||
targets: ["TokamakStaticDemo"]
|
||||
name: "TokamakStaticHTMLDemo",
|
||||
targets: ["TokamakStaticHTMLDemo"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakGTK",
|
||||
|
@ -47,32 +45,40 @@ let package = Package(
|
|||
),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(
|
||||
url: "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
.upToNextMinor(from: "0.10.0")
|
||||
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"
|
||||
),
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0"),
|
||||
.package(url: "https://github.com/swiftwasm/OpenCombineJS.git", .upToNextMinor(from: "0.1.1")),
|
||||
.package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets 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"]
|
||||
dependencies: [
|
||||
.product(
|
||||
name: "OpenCombineShim",
|
||||
package: "OpenCombine"
|
||||
),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakShim",
|
||||
|
@ -105,9 +111,15 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "TokamakGTK",
|
||||
dependencies: ["TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers", "CombineShim"]
|
||||
dependencies: [
|
||||
"TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers",
|
||||
.product(
|
||||
name: "OpenCombineShim",
|
||||
package: "OpenCombine"
|
||||
),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "TokamakGTKDemo",
|
||||
dependencies: ["TokamakGTK"],
|
||||
resources: [.copy("logo-header.png")]
|
||||
|
@ -118,35 +130,44 @@ let package = Package(
|
|||
"TokamakCore",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "TokamakCoreBenchmark",
|
||||
dependencies: [
|
||||
"Benchmark",
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "TokamakStaticHTMLBenchmark",
|
||||
dependencies: [
|
||||
"Benchmark",
|
||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||
"TokamakStaticHTML",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDOM",
|
||||
dependencies: [
|
||||
"CombineShim",
|
||||
"OpenCombineJS",
|
||||
"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",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "TokamakDemo",
|
||||
dependencies: [
|
||||
"TokamakShim",
|
||||
|
@ -156,10 +177,16 @@ let package = Package(
|
|||
condition: .when(platforms: [.wasi])
|
||||
),
|
||||
],
|
||||
resources: [.copy("logo-header.png")]
|
||||
resources: [.copy("logo-header.png")],
|
||||
linkerSettings: [
|
||||
.unsafeFlags(
|
||||
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
|
||||
.when(platforms: [.wasi])
|
||||
),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakStaticDemo",
|
||||
.executableTarget(
|
||||
name: "TokamakStaticHTMLDemo",
|
||||
dependencies: [
|
||||
"TokamakStaticHTML",
|
||||
]
|
||||
|
@ -168,18 +195,40 @@ let package = Package(
|
|||
name: "TokamakTestRenderer",
|
||||
dependencies: ["TokamakCore"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakLayoutTests",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakStaticHTML",
|
||||
.product(
|
||||
name: "SnapshotTesting",
|
||||
package: "swift-snapshot-testing",
|
||||
condition: .when(platforms: [.macOS])
|
||||
),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakReconcilerTests",
|
||||
dependencies: [
|
||||
"TokamakCore",
|
||||
"TokamakTestRenderer",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TokamakTests",
|
||||
dependencies: ["TokamakTestRenderer"]
|
||||
),
|
||||
// FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved
|
||||
// Currently, when multiple modules that have conflicting `ViewDeferredToRenderer`
|
||||
// implementations are linked in the same binary, only a single one is used with no defined
|
||||
// behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution
|
||||
// that isn't prone to these hard to debug errors.
|
||||
// .testTarget(
|
||||
// name: "TokamakStaticHTMLTests",
|
||||
// dependencies: ["TokamakStaticHTML"]
|
||||
// ),
|
||||
.testTarget(
|
||||
name: "TokamakStaticHTMLTests",
|
||||
dependencies: [
|
||||
"TokamakStaticHTML",
|
||||
.product(
|
||||
name: "SnapshotTesting",
|
||||
package: "swift-snapshot-testing",
|
||||
condition: .when(platforms: [.macOS])
|
||||
),
|
||||
],
|
||||
exclude: ["__Snapshots__", "RenderingTests/__Snapshots__"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
191
README.md
191
README.md
|
@ -52,6 +52,7 @@ struct Counter: View {
|
|||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup("Counter Demo") {
|
||||
|
@ -59,10 +60,6 @@ struct CounterApp: App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @main attribute is not supported in SwiftPM apps.
|
||||
// See https://bugs.swift.org/browse/SR-12683 for more details.
|
||||
CounterApp.main()
|
||||
```
|
||||
|
||||
### Arbitrary HTML
|
||||
|
@ -82,6 +79,40 @@ struct SVGCircle: View {
|
|||
}
|
||||
```
|
||||
|
||||
`HTML` doesn't support event listeners, and is declared in the `TokamakStaticHTML` module, which `TokamakDOM` re-exports. The benefit of `HTML` is that you can use it for static rendering in libraries like [TokamakVapor](https://github.com/TokamakUI/TokamakVapor) and [TokamakPublish](https://github.com/TokamakUI/TokamakPublish).
|
||||
|
||||
Another option is the `DynamicHTML` view provided by the `TokamakDOM` module, which has a `listeners` property with a corresponding initializer parameter. You can pass closures that can handle `onclick`, `onmouseover` and other DOM events for you in the `listeners` dictionary. Check out [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers) for the full list.
|
||||
|
||||
An example of mouse events handling with `DynamicHTML` would look like this:
|
||||
|
||||
```swift
|
||||
struct MouseEventsView: View {
|
||||
@State var position: CGPoint = .zero
|
||||
@State var isMouseButtonDown: Bool = false
|
||||
|
||||
var body: some View {
|
||||
DynamicHTML(
|
||||
"div",
|
||||
["style": "width: 200px; height: 200px; background-color: red;"],
|
||||
listeners: [
|
||||
"mousemove": { event in
|
||||
guard
|
||||
let x = event.offsetX.jsValue.number,
|
||||
let y = event.offsetY.jsValue.number
|
||||
else { return }
|
||||
|
||||
position = CGPoint(x: x, y: y)
|
||||
},
|
||||
"mousedown": { _ in isMouseButtonDown = true },
|
||||
"mouseup": { _ in isMouseButtonDown = false },
|
||||
]
|
||||
) {
|
||||
Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Arbitrary styles and scripts
|
||||
|
||||
While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript,
|
||||
|
@ -107,23 +138,63 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
|
|||
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
||||
app.
|
||||
|
||||
## Requirements for app developers
|
||||
### Fiber renderers
|
||||
|
||||
- macOS 10.15 and Xcode 11.4 or later. macOS 11.0 and Xcode 12.0 or later are required if you're
|
||||
building a multi-platform app with Tokamak that also needs to support SwiftUI on macOS.
|
||||
- [Swift 5.2 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use 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.
|
||||
|
||||
You can specify which reconciler to use in your `App`'s configuration:
|
||||
|
||||
```swift
|
||||
struct CounterApp: App {
|
||||
static let _configuration: _AppConfiguration = .init(
|
||||
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
|
||||
reconciler: .fiber(useDynamicLayout: true)
|
||||
)
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("Counter Demo") {
|
||||
Counter(count: 5, limit: 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
|
||||
|
||||
## Requirements
|
||||
|
||||
### For app developers
|
||||
|
||||
- macOS 11 and Xcode 13.2 or later when using VS Code. macOS 12 and Xcode 13.3 or later are recommended if
|
||||
you'd like to use Xcode for auto-completion, or when developing multi-platform apps that target WebAssembly
|
||||
and macOS at the same time.
|
||||
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
|
||||
Other Linux distributions are currently not supported.
|
||||
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
||||
|
||||
## Requirements for app users
|
||||
### For users of apps depending on Tokamak
|
||||
|
||||
Any browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) should work, which currently includes:
|
||||
Any recent browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) and [required
|
||||
JavaScript features](https://caniuse.com/?search=finalizationregistry) should work, which currently includes:
|
||||
|
||||
- Edge 84+
|
||||
- Firefox 79+
|
||||
- Chrome 84+
|
||||
- Desktop Safari 14.1+
|
||||
- Mobile Safari 14.8+
|
||||
|
||||
If you need to support older browser versions, you'll have to build with
|
||||
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags
|
||||
when compiling. This should lower browser requirements to these versions:
|
||||
|
||||
- Edge 16+
|
||||
- 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
|
||||
|
||||
|
@ -138,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.9.0 or greater:
|
||||
If you had `carton` installed before this, make sure you have version 0.15.0 or greater:
|
||||
|
||||
```
|
||||
carton --version
|
||||
|
@ -170,6 +241,26 @@ carton dev
|
|||
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
||||
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
||||
|
||||
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
|
||||
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
|
||||
|
||||
## Security
|
||||
|
||||
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
||||
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
|
||||
|
@ -205,63 +296,14 @@ doesn't provide an official build of the extension on the VSCode Marketplace unf
|
|||
|
||||
## Contributing
|
||||
|
||||
### Modular structure
|
||||
All contributions, no matter how small, are very welcome. You don't have to be a web developer or a
|
||||
SwiftUI expert to meaningfully contribute. In fact, by checking out how some of the simplest views are
|
||||
implemented in Tokamak you may learn more how SwiftUI may work under the hood.
|
||||
|
||||
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
|
||||
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.
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||
|
@ -283,9 +325,10 @@ appreciated and helps in maintaining the project.
|
|||
## Maintainers
|
||||
|
||||
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
||||
[David Hunt](https://github.com/foscomputerservices),
|
||||
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
|
||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
|
||||
[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,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
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
// Created by Carson Katri on 7/16/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
/// Provides the ability to set the title of the Scene.
|
||||
public protocol _TitledApp {
|
||||
|
@ -28,7 +28,10 @@ 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: AnyPublisher<ScenePhase, Never> { get }
|
||||
|
@ -36,14 +39,38 @@ public protocol App: _TitledApp {
|
|||
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||
|
||||
static var _configuration: _AppConfiguration { get }
|
||||
|
||||
static func main()
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
public extension App {
|
||||
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!
|
||||
}
|
||||
|
@ -173,6 +177,7 @@ struct DefaultAppStorageEnvironmentKey: EnvironmentKey {
|
|||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
@_spi(TokamakCore)
|
||||
var _defaultAppStorage: _StorageProvider? {
|
||||
get {
|
||||
self[DefaultAppStorageEnvironmentKey.self]
|
||||
|
|
|
@ -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,7 +15,7 @@
|
|||
// Created by Carson Katri on 7/16/20.
|
||||
//
|
||||
|
||||
@_functionBuilder
|
||||
@resultBuilder
|
||||
public enum SceneBuilder {
|
||||
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
|
||||
content
|
||||
|
@ -29,7 +29,14 @@ 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)])
|
||||
_TupleScene(
|
||||
(c0, c1),
|
||||
children: [_AnyScene(c0), _AnyScene(c1)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,7 +44,15 @@ public extension SceneBuilder {
|
|||
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
||||
where C0: Scene, C1: Scene, C2: Scene
|
||||
{
|
||||
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
|
||||
_TupleScene(
|
||||
(c0, c1, c2),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +65,13 @@ public extension SceneBuilder {
|
|||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)]
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +86,14 @@ public extension SceneBuilder {
|
|||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||
_TupleScene(
|
||||
(c0, c1, c2, c3, c4),
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)]
|
||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +118,15 @@ public extension SceneBuilder {
|
|||
_AnyScene(c3),
|
||||
_AnyScene(c4),
|
||||
_AnyScene(c5),
|
||||
]
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +153,16 @@ public extension SceneBuilder {
|
|||
_AnyScene(c4),
|
||||
_AnyScene(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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +191,17 @@ public extension SceneBuilder {
|
|||
_AnyScene(c5),
|
||||
_AnyScene(c6),
|
||||
_AnyScene(c7),
|
||||
]
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +232,18 @@ public extension SceneBuilder {
|
|||
_AnyScene(c6),
|
||||
_AnyScene(c7),
|
||||
_AnyScene(c8),
|
||||
]
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
$0.visit(c8)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +276,19 @@ public extension SceneBuilder {
|
|||
_AnyScene(c7),
|
||||
_AnyScene(c8),
|
||||
_AnyScene(c9),
|
||||
]
|
||||
],
|
||||
visit: {
|
||||
$0.visit(c0)
|
||||
$0.visit(c1)
|
||||
$0.visit(c2)
|
||||
$0.visit(c3)
|
||||
$0.visit(c4)
|
||||
$0.visit(c5)
|
||||
$0.visit(c6)
|
||||
$0.visit(c7)
|
||||
$0.visit(c8)
|
||||
$0.visit(c9)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) -> ()
|
||||
|
|
|
@ -63,6 +63,7 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
|||
self.content = content()
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var body: Never {
|
||||
neverScene("WindowGroup")
|
||||
}
|
||||
|
@ -74,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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -31,26 +31,36 @@ 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 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.")
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ public struct _AnyScene: Scene {
|
|||
}
|
||||
}
|
||||
|
||||
@_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 {
|
||||
|
|
|
@ -23,7 +23,8 @@ protocol EnvironmentReader {
|
|||
mutating func setContent(from values: EnvironmentValues)
|
||||
}
|
||||
|
||||
@propertyWrapper public struct Environment<Value>: DynamicProperty {
|
||||
@propertyWrapper
|
||||
public struct Environment<Value>: DynamicProperty {
|
||||
enum Content {
|
||||
case keyPath(KeyPath<EnvironmentValues, Value>)
|
||||
case value(Value)
|
||||
|
|
|
@ -17,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
|
||||
|
||||
|
@ -32,7 +45,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo
|
|||
|
||||
public typealias Body = Never
|
||||
|
||||
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
public func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||
values[keyPath: keyPath] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,12 +15,14 @@
|
|||
// Created by Carson Katri on 7/7/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
@propertyWrapper
|
||||
public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||
where ObjectType: ObservableObject
|
||||
{
|
||||
@dynamicMemberLookup public struct Wrapper {
|
||||
@dynamicMemberLookup
|
||||
public struct Wrapper {
|
||||
internal let root: ObjectType
|
||||
public subscript<Subject>(
|
||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
public struct EnvironmentValues: CustomStringConvertible {
|
||||
public var description: String {
|
||||
|
@ -43,6 +43,22 @@ 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 IsEnabledKey: EnvironmentKey {
|
||||
|
@ -60,7 +76,7 @@ public extension EnvironmentValues {
|
|||
}
|
||||
}
|
||||
|
||||
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
||||
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
|
||||
let environmentValues: EnvironmentValues
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/11/21.
|
||||
//
|
||||
|
||||
/// A modifier that resolves to a concrete modifier in an environment.
|
||||
public protocol EnvironmentalModifier: ViewModifier {
|
||||
associatedtype ResolvedModifier: ViewModifier
|
||||
func resolve(in environment: EnvironmentValues) -> ResolvedModifier
|
||||
static var _requiresMainThread: Bool { get }
|
||||
}
|
||||
|
||||
private struct EnvironmentalModifierResolver<M>: ViewModifier, EnvironmentReader
|
||||
where M: EnvironmentalModifier
|
||||
{
|
||||
let modifier: M
|
||||
var resolved: M.ResolvedModifier!
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.modifier(resolved)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
resolved = modifier.resolve(in: values)
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnvironmentalModifier {
|
||||
static var _requiresMainThread: Bool { true }
|
||||
|
||||
func body(content: _ViewModifier_Content<Self>) -> some View {
|
||||
content.modifier(EnvironmentalModifierResolver(modifier: self))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/18/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Used to identify an alignment guide.
|
||||
///
|
||||
/// Typically, you would define an alignment guide inside
|
||||
/// an extension on `HorizontalAlignment` or `VerticalAlignment`:
|
||||
///
|
||||
/// extension HorizontalAlignment {
|
||||
/// private enum MyAlignmentGuide: AlignmentID {
|
||||
/// static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
/// return 0.0
|
||||
/// }
|
||||
/// }
|
||||
/// public static let myAlignmentGuide = Self(MyAlignmentGuide.self)
|
||||
/// }
|
||||
///
|
||||
/// Which you can then use with the `alignmentGuide` modifier:
|
||||
///
|
||||
/// VStack(alignment: .myAlignmentGuide) {
|
||||
/// Text("Align Leading")
|
||||
/// .border(.red)
|
||||
/// .alignmentGuide(.myAlignmentGuide) { $0[.leading] }
|
||||
/// Text("Align Trailing")
|
||||
/// .border(.blue)
|
||||
/// .alignmentGuide(.myAlignmentGuide) { $0[.trailing] }
|
||||
/// }
|
||||
/// .border(.green)
|
||||
public protocol AlignmentID {
|
||||
/// The default value for this alignment guide
|
||||
/// when not set via the `alignmentGuide` modifier.
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat
|
||||
}
|
||||
|
||||
/// An alignment position along the horizontal axis.
|
||||
@frozen
|
||||
public struct HorizontalAlignment: Equatable {
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
let id: AlignmentID.Type
|
||||
|
||||
public init(_ id: AlignmentID.Type) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
extension HorizontalAlignment {
|
||||
public static let leading = Self(Leading.self)
|
||||
|
||||
private enum Leading: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
public static let center = Self(Center.self)
|
||||
|
||||
private enum Center: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.width / 2
|
||||
}
|
||||
}
|
||||
|
||||
public static let trailing = Self(Trailing.self)
|
||||
|
||||
private enum Trailing: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct VerticalAlignment: Equatable {
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
let id: AlignmentID.Type
|
||||
|
||||
public init(_ id: AlignmentID.Type) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
extension VerticalAlignment {
|
||||
public static let top = Self(Top.self)
|
||||
private enum Top: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
public static let center = Self(Center.self)
|
||||
private enum Center: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
public static let bottom = Self(Bottom.self)
|
||||
private enum Bottom: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.height
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add baseline vertical alignment guides.
|
||||
// public static let firstTextBaseline: VerticalAlignment
|
||||
// public static let lastTextBaseline: VerticalAlignment
|
||||
}
|
||||
|
||||
/// An alignment in both axes.
|
||||
public struct Alignment: Equatable {
|
||||
public var horizontal: HorizontalAlignment
|
||||
public var vertical: VerticalAlignment
|
||||
|
||||
public init(
|
||||
horizontal: HorizontalAlignment,
|
||||
vertical: VerticalAlignment
|
||||
) {
|
||||
self.horizontal = horizontal
|
||||
self.vertical = vertical
|
||||
}
|
||||
|
||||
public static let topLeading = Self(horizontal: .leading, vertical: .top)
|
||||
public static let top = Self(horizontal: .center, vertical: .top)
|
||||
public static let topTrailing = Self(horizontal: .trailing, vertical: .top)
|
||||
public static let leading = Self(horizontal: .leading, vertical: .center)
|
||||
public static let center = Self(horizontal: .center, vertical: .center)
|
||||
public static let trailing = Self(horizontal: .trailing, vertical: .center)
|
||||
public static let bottomLeading = Self(horizontal: .leading, vertical: .bottom)
|
||||
public static let bottom = Self(horizontal: .center, vertical: .bottom)
|
||||
public static let bottomTrailing = Self(horizontal: .trailing, vertical: .bottom)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/31/22.
|
||||
//
|
||||
|
||||
/// A type that can visit an `App`.
|
||||
public protocol AppVisitor: ViewVisitor {
|
||||
func visit<A: App>(_ app: A)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/31/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension FiberReconciler.Fiber {
|
||||
enum Content {
|
||||
/// The underlying `App` instance and a function to visit it generically.
|
||||
case app(Any, visit: (AppVisitor) -> ())
|
||||
/// The underlying `Scene` instance and a function to visit it generically.
|
||||
case scene(Any, visit: (SceneVisitor) -> ())
|
||||
/// The underlying `View` instance and a function to visit it generically.
|
||||
case view(Any, visit: (ViewVisitor) -> ())
|
||||
}
|
||||
|
||||
/// Create a `Content` value for a given `App`.
|
||||
func content<A: App>(for app: A) -> Content {
|
||||
.app(
|
||||
app,
|
||||
visit: { [weak self] in
|
||||
guard case let .app(app, _) = self?.content else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(app as! A)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a `Content` value for a given `Scene`.
|
||||
func content<S: Scene>(for scene: S) -> Content {
|
||||
.scene(
|
||||
scene,
|
||||
visit: { [weak self] in
|
||||
guard case let .scene(scene, _) = self?.content else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(scene as! S)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a `Content` value for a given `View`.
|
||||
func content<V: View>(for view: V) -> Content {
|
||||
.view(
|
||||
view,
|
||||
visit: { [weak self] in
|
||||
guard case let .view(view, _) = self?.content else { return }
|
||||
// swiftlint:disable:next force_cast
|
||||
$0.visit(view as! V)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/30/22.
|
||||
//
|
||||
|
||||
extension FiberReconciler.Fiber: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
let memoryAddress = String(format: "%010p", unsafeBitCast(self, to: Int.self))
|
||||
if case let .view(view, _) = content,
|
||||
let text = view as? Text
|
||||
{
|
||||
return "Text(\"\(text.storage.rawText)\") (\(memoryAddress))"
|
||||
}
|
||||
return "\(typeInfo?.name ?? "Unknown") (\(memoryAddress))"
|
||||
}
|
||||
|
||||
private func flush(level: Int = 0) -> String {
|
||||
let spaces = String(repeating: " ", count: level)
|
||||
let geometry = geometry ?? .init(
|
||||
origin: .init(origin: .zero),
|
||||
dimensions: .init(size: .zero, alignmentGuides: [:]),
|
||||
proposal: .unspecified
|
||||
)
|
||||
return """
|
||||
\(spaces)\(String(describing: typeInfo?.type ?? Any.self)
|
||||
.split(separator: "<")[0])\(element != nil ? "(\(element!))" : "") {\(element != nil ?
|
||||
"\n\(spaces)geometry: \(geometry)" :
|
||||
"")
|
||||
\(child?.flush(level: level + 2) ?? "")
|
||||
\(spaces)}
|
||||
\(sibling?.flush(level: level) ?? "")
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,626 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombineShim
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
@_spi(TokamakCore)
|
||||
public extension FiberReconciler {
|
||||
/// A manager for a single `View`.
|
||||
///
|
||||
/// There are always 2 `Fiber`s for every `View` in the tree,
|
||||
/// a current `Fiber`, and a work in progress `Fiber`.
|
||||
/// They point to each other using the `alternate` property.
|
||||
///
|
||||
/// The current `Fiber` represents the `View` as it is currently rendered on the screen.
|
||||
/// The work in progress `Fiber` (the `alternate` of current),
|
||||
/// is used in the reconciler to compute the new tree.
|
||||
///
|
||||
/// When reconciling, the tree is recomputed from
|
||||
/// the root of the state change on the work in progress `Fiber`.
|
||||
/// Each node in the fiber tree is updated to apply any changes,
|
||||
/// and a list of mutations needed to get the rendered output to match is created.
|
||||
///
|
||||
/// After the entire tree has been traversed, the current and work in progress trees are swapped,
|
||||
/// making the updated tree the current one,
|
||||
/// and leaving the previous current tree available to apply future changes on.
|
||||
final class Fiber {
|
||||
weak var reconciler: FiberReconciler<Renderer>?
|
||||
|
||||
/// The underlying value behind this `Fiber`. Either a `Scene` or `View` instance.
|
||||
///
|
||||
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance,
|
||||
/// and captures a weak reference to `self` in the visitor function,
|
||||
/// which requires all stored properties be set before capturing.
|
||||
@_spi(TokamakCore)
|
||||
public var content: Content!
|
||||
|
||||
/// Outputs from evaluating `View._makeView`
|
||||
///
|
||||
/// Stored as an IUO because creating `ViewOutputs` depends on
|
||||
/// the `bindProperties` method, which requires
|
||||
/// all stored properties be set before using.
|
||||
/// `outputs` is guaranteed to be set in the initializer.
|
||||
var outputs: ViewOutputs!
|
||||
|
||||
/// The erased `Layout` to use for this content.
|
||||
///
|
||||
/// Stored as an IUO because it uses `bindProperties` to create the underlying instance.
|
||||
var layout: AnyLayout?
|
||||
|
||||
/// The identity of this `View`
|
||||
var id: Identity?
|
||||
|
||||
/// The mounted element, if this is a `Renderer` primitive.
|
||||
var element: Renderer.ElementType?
|
||||
|
||||
/// The index of this element in its `elementParent`
|
||||
var elementIndex: Int?
|
||||
|
||||
/// The first child node.
|
||||
@_spi(TokamakCore)
|
||||
public var child: Fiber?
|
||||
|
||||
/// This node's right sibling.
|
||||
@_spi(TokamakCore)
|
||||
public var sibling: Fiber?
|
||||
|
||||
/// An unowned reference to the parent node.
|
||||
///
|
||||
/// Parent references are `unowned` (as opposed to `weak`)
|
||||
/// because the parent will always exist if a child does.
|
||||
/// If the parent is released, the child is released with it.
|
||||
@_spi(TokamakCore)
|
||||
public unowned var parent: Fiber?
|
||||
|
||||
/// The nearest parent that can be mounted on.
|
||||
unowned var elementParent: Fiber?
|
||||
|
||||
/// The nearest parent that receives preferences.
|
||||
unowned var preferenceParent: Fiber?
|
||||
|
||||
/// The cached type information for the underlying `View`.
|
||||
var typeInfo: TypeInfo?
|
||||
|
||||
/// Boxes that store `State` data.
|
||||
var state: [PropertyInfo: MutableStorage] = [:]
|
||||
|
||||
/// Subscribed `Cancellable`s keyed with the property contained the observable.
|
||||
///
|
||||
/// Each time properties are bound, a new subscription could be created.
|
||||
/// When the subscription is overridden, the old cancellable is released.
|
||||
var subscriptions: [PropertyInfo: AnyCancellable] = [:]
|
||||
|
||||
/// Storage for `PreferenceKey` values as they are passed up the tree.
|
||||
var preferences: _PreferenceStore?
|
||||
|
||||
/// The computed dimensions and origin.
|
||||
var geometry: ViewGeometry?
|
||||
|
||||
/// The WIP node if this is current, or the current node if this is WIP.
|
||||
@_spi(TokamakCore)
|
||||
public weak var alternate: Fiber?
|
||||
|
||||
var createAndBindAlternate: (() -> Fiber?)?
|
||||
|
||||
/// A box holding a value for an `@State` property wrapper.
|
||||
/// Will call `onSet` (usually a `Reconciler.reconcile` call) when updated.
|
||||
final class MutableStorage {
|
||||
private(set) var value: Any
|
||||
let onSet: () -> ()
|
||||
|
||||
func setValue(_ newValue: Any, with transaction: Transaction) {
|
||||
value = newValue
|
||||
onSet()
|
||||
}
|
||||
|
||||
init(initialValue: Any, onSet: @escaping () -> ()) {
|
||||
value = initialValue
|
||||
self.onSet = onSet
|
||||
}
|
||||
}
|
||||
|
||||
public enum Identity: Hashable {
|
||||
case explicit(AnyHashable)
|
||||
case structural(index: Int)
|
||||
}
|
||||
|
||||
init<V: View>(
|
||||
_ view: inout V,
|
||||
element: Renderer.ElementType?,
|
||||
parent: Fiber?,
|
||||
elementParent: Fiber?,
|
||||
preferenceParent: Fiber?,
|
||||
elementIndex: Int?,
|
||||
traits: _ViewTraitStore?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.reconciler = reconciler
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
self.preferenceParent = preferenceParent
|
||||
typeInfo = TokamakCore.typeInfo(of: V.self)
|
||||
|
||||
let environment = parent?.outputs.environment ?? .init(.init())
|
||||
bindProperties(to: &view, typeInfo, environment.environment)
|
||||
var updateView = view
|
||||
let viewInputs = ViewInputs(
|
||||
content: view,
|
||||
updateContent: { $0(&updateView) },
|
||||
environment: environment,
|
||||
traits: traits,
|
||||
preferenceStore: preferences
|
||||
)
|
||||
outputs = V._makeView(viewInputs)
|
||||
if let preferenceStore = outputs.preferenceStore {
|
||||
preferences = preferenceStore
|
||||
}
|
||||
view = updateView
|
||||
content = content(for: view)
|
||||
|
||||
if let element = element {
|
||||
self.element = element
|
||||
} else if Renderer.isPrimitive(view) {
|
||||
self.element = .init(
|
||||
from: .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
if self.element != nil {
|
||||
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||
}
|
||||
|
||||
// Only specify an `elementIndex` if we have an element.
|
||||
if self.element != nil {
|
||||
self.elementIndex = elementIndex
|
||||
}
|
||||
|
||||
let alternateView = view
|
||||
createAndBindAlternate = { [weak self] in
|
||||
guard let self = self else { return nil }
|
||||
// Create the alternate lazily
|
||||
let alternate = Fiber(
|
||||
bound: alternateView,
|
||||
state: self.state,
|
||||
subscriptions: self.subscriptions,
|
||||
preferences: self.preferences,
|
||||
layout: self.layout,
|
||||
alternate: self,
|
||||
outputs: self.outputs,
|
||||
typeInfo: self.typeInfo,
|
||||
element: self.element,
|
||||
parent: self.parent?.alternate,
|
||||
elementParent: self.elementParent?.alternate,
|
||||
preferenceParent: self.preferenceParent?.alternate,
|
||||
reconciler: reconciler
|
||||
)
|
||||
self.alternate = alternate
|
||||
if self.parent?.child === self {
|
||||
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
|
||||
} else {
|
||||
// Find our left sibling.
|
||||
var node = self.parent?.child
|
||||
while node?.sibling !== self {
|
||||
guard node?.sibling != nil else { return alternate }
|
||||
node = node?.sibling
|
||||
}
|
||||
if node?.sibling === self {
|
||||
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
|
||||
}
|
||||
}
|
||||
return alternate
|
||||
}
|
||||
}
|
||||
|
||||
init<V: View>(
|
||||
bound view: V,
|
||||
state: [PropertyInfo: MutableStorage],
|
||||
subscriptions: [PropertyInfo: AnyCancellable],
|
||||
preferences: _PreferenceStore?,
|
||||
layout: AnyLayout!,
|
||||
alternate: Fiber,
|
||||
outputs: ViewOutputs,
|
||||
typeInfo: TypeInfo?,
|
||||
element: Renderer.ElementType?,
|
||||
parent: FiberReconciler<Renderer>.Fiber?,
|
||||
elementParent: Fiber?,
|
||||
preferenceParent: Fiber?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.alternate = alternate
|
||||
self.reconciler = reconciler
|
||||
self.element = element
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
self.preferenceParent = preferenceParent
|
||||
self.typeInfo = typeInfo
|
||||
self.outputs = outputs
|
||||
self.state = state
|
||||
self.subscriptions = subscriptions
|
||||
self.preferences = preferences
|
||||
if element != nil {
|
||||
self.layout = layout
|
||||
}
|
||||
content = content(for: view)
|
||||
}
|
||||
|
||||
private func bindProperties<T>(
|
||||
to content: inout T,
|
||||
_ typeInfo: TypeInfo?,
|
||||
_ environment: EnvironmentValues
|
||||
) {
|
||||
var erased: Any = content
|
||||
bindProperties(to: &erased, typeInfo, environment)
|
||||
// swiftlint:disable:next force_cast
|
||||
content = erased as! T
|
||||
}
|
||||
|
||||
/// Collect `DynamicProperty`s and link their state changes to the reconciler.
|
||||
private func bindProperties(
|
||||
to content: inout Any,
|
||||
_ typeInfo: TypeInfo?,
|
||||
_ environment: EnvironmentValues
|
||||
) {
|
||||
guard let typeInfo = typeInfo else { return }
|
||||
|
||||
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
||||
var value = property.get(from: content)
|
||||
// Bind nested properties.
|
||||
bindProperties(to: &value, TokamakCore.typeInfo(of: property.type), environment)
|
||||
// Create boxes for `@State` and other mutable properties.
|
||||
if var storage = value as? WritableValueStorage {
|
||||
let box = self.state[property] ?? MutableStorage(
|
||||
initialValue: storage.anyInitialValue,
|
||||
onSet: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.reconciler?.fiberChanged(self)
|
||||
}
|
||||
)
|
||||
state[property] = box
|
||||
storage.getter = { box.value }
|
||||
storage.setter = { box.setValue($0, with: $1) }
|
||||
value = storage
|
||||
// Create boxes for `@StateObject` and other immutable properties.
|
||||
} else if var storage = value as? ValueStorage {
|
||||
let box = self.state[property] ?? MutableStorage(
|
||||
initialValue: storage.anyInitialValue,
|
||||
onSet: {}
|
||||
)
|
||||
state[property] = box
|
||||
storage.getter = { box.value }
|
||||
value = storage
|
||||
// Read from the environment.
|
||||
} else if var environmentReader = value as? EnvironmentReader {
|
||||
environmentReader.setContent(from: environment)
|
||||
value = environmentReader
|
||||
}
|
||||
// Subscribe to observable properties.
|
||||
if let observed = value as? ObservedProperty {
|
||||
subscriptions[property] = observed.objectWillChange.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.reconciler?.fiberChanged(self)
|
||||
}
|
||||
}
|
||||
property.set(value: value, on: &content)
|
||||
}
|
||||
if var environmentReader = content as? EnvironmentReader {
|
||||
environmentReader.setContent(from: environment)
|
||||
content = environmentReader
|
||||
}
|
||||
}
|
||||
|
||||
/// Call `update()` on each `DynamicProperty` in the type.
|
||||
private func updateDynamicProperties(
|
||||
of content: inout Any,
|
||||
_ typeInfo: TypeInfo?
|
||||
) {
|
||||
guard let typeInfo = typeInfo else { return }
|
||||
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
||||
var value = property.get(from: content)
|
||||
// Update nested properties.
|
||||
updateDynamicProperties(of: &value, TokamakCore.typeInfo(of: property.type))
|
||||
// swiftlint:disable:next force_cast
|
||||
var dynamicProperty = value as! DynamicProperty
|
||||
dynamicProperty.update()
|
||||
property.set(value: dynamicProperty, on: &content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update each `DynamicProperty` in our content.
|
||||
func updateDynamicProperties() {
|
||||
guard let content = content else { return }
|
||||
switch content {
|
||||
case .app(var app, let visit):
|
||||
updateDynamicProperties(of: &app, typeInfo)
|
||||
self.content = .app(app, visit: visit)
|
||||
case .scene(var scene, let visit):
|
||||
updateDynamicProperties(of: &scene, typeInfo)
|
||||
self.content = .scene(scene, visit: visit)
|
||||
case .view(var view, let visit):
|
||||
updateDynamicProperties(of: &view, typeInfo)
|
||||
self.content = .view(view, visit: visit)
|
||||
}
|
||||
}
|
||||
|
||||
func update<V: View>(
|
||||
with view: inout V,
|
||||
elementIndex: Int?,
|
||||
traits: _ViewTraitStore?
|
||||
) -> Renderer.ElementType.Content? {
|
||||
typeInfo = TokamakCore.typeInfo(of: V.self)
|
||||
|
||||
self.elementIndex = elementIndex
|
||||
|
||||
let environment = parent?.outputs.environment ?? .init(.init())
|
||||
bindProperties(to: &view, typeInfo, environment.environment)
|
||||
var updateView = view
|
||||
let inputs = ViewInputs(
|
||||
content: view,
|
||||
updateContent: {
|
||||
$0(&updateView)
|
||||
},
|
||||
environment: environment,
|
||||
traits: traits,
|
||||
preferenceStore: preferences
|
||||
)
|
||||
outputs = V._makeView(inputs)
|
||||
view = updateView
|
||||
content = content(for: view)
|
||||
|
||||
if element != nil {
|
||||
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||
}
|
||||
|
||||
if Renderer.isPrimitive(view) {
|
||||
return .init(from: view, useDynamicLayout: reconciler?.renderer.useDynamicLayout ?? false)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init<A: App>(
|
||||
_ app: inout A,
|
||||
rootElement: Renderer.ElementType,
|
||||
rootEnvironment: EnvironmentValues,
|
||||
reconciler: FiberReconciler<Renderer>
|
||||
) {
|
||||
self.reconciler = reconciler
|
||||
child = nil
|
||||
sibling = nil
|
||||
// `App`s are always the root, so they can have no parent.
|
||||
parent = nil
|
||||
elementParent = nil
|
||||
preferenceParent = nil
|
||||
element = rootElement
|
||||
typeInfo = TokamakCore.typeInfo(of: A.self)
|
||||
bindProperties(to: &app, typeInfo, rootEnvironment)
|
||||
var updateApp = app
|
||||
outputs = .init(
|
||||
inputs: .init(
|
||||
content: app,
|
||||
updateContent: {
|
||||
$0(&updateApp)
|
||||
},
|
||||
environment: .init(rootEnvironment),
|
||||
traits: .init(),
|
||||
preferenceStore: preferences
|
||||
)
|
||||
)
|
||||
if let preferenceStore = outputs.preferenceStore {
|
||||
preferences = preferenceStore
|
||||
}
|
||||
app = updateApp
|
||||
content = content(for: app)
|
||||
|
||||
layout = .init(RootLayout(renderer: reconciler.renderer))
|
||||
|
||||
let alternateApp = app
|
||||
createAndBindAlternate = { [weak self] in
|
||||
guard let self = self else { return nil }
|
||||
// Create the alternate lazily
|
||||
let alternate = Fiber(
|
||||
bound: alternateApp,
|
||||
state: self.state,
|
||||
subscriptions: self.subscriptions,
|
||||
preferences: self.preferences,
|
||||
layout: self.layout,
|
||||
alternate: self,
|
||||
outputs: self.outputs,
|
||||
typeInfo: self.typeInfo,
|
||||
element: self.element,
|
||||
reconciler: reconciler
|
||||
)
|
||||
self.alternate = alternate
|
||||
return alternate
|
||||
}
|
||||
}
|
||||
|
||||
init<A: App>(
|
||||
bound app: A,
|
||||
state: [PropertyInfo: MutableStorage],
|
||||
subscriptions: [PropertyInfo: AnyCancellable],
|
||||
preferences: _PreferenceStore?,
|
||||
layout: AnyLayout?,
|
||||
alternate: Fiber,
|
||||
outputs: SceneOutputs,
|
||||
typeInfo: TypeInfo?,
|
||||
element: Renderer.ElementType?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.alternate = alternate
|
||||
self.reconciler = reconciler
|
||||
self.element = element
|
||||
child = nil
|
||||
sibling = nil
|
||||
parent = nil
|
||||
elementParent = nil
|
||||
preferenceParent = nil
|
||||
self.typeInfo = typeInfo
|
||||
self.outputs = outputs
|
||||
self.state = state
|
||||
self.subscriptions = subscriptions
|
||||
self.preferences = preferences
|
||||
self.layout = layout
|
||||
content = content(for: app)
|
||||
}
|
||||
|
||||
init<S: Scene>(
|
||||
_ scene: inout S,
|
||||
element: Renderer.ElementType?,
|
||||
parent: Fiber?,
|
||||
elementParent: Fiber?,
|
||||
preferenceParent: Fiber?,
|
||||
environment: EnvironmentBox?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.reconciler = reconciler
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
self.element = element
|
||||
self.preferenceParent = preferenceParent
|
||||
typeInfo = TokamakCore.typeInfo(of: S.self)
|
||||
|
||||
let environment = environment ?? parent?.outputs.environment ?? .init(.init())
|
||||
bindProperties(to: &scene, typeInfo, environment.environment)
|
||||
var updateScene = scene
|
||||
outputs = S._makeScene(
|
||||
.init(
|
||||
content: scene,
|
||||
updateContent: {
|
||||
$0(&updateScene)
|
||||
},
|
||||
environment: environment,
|
||||
traits: .init(),
|
||||
preferenceStore: preferences
|
||||
)
|
||||
)
|
||||
if let preferenceStore = outputs.preferenceStore {
|
||||
preferences = preferenceStore
|
||||
}
|
||||
scene = updateScene
|
||||
content = content(for: scene)
|
||||
|
||||
if element != nil {
|
||||
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||
}
|
||||
|
||||
let alternateScene = scene
|
||||
createAndBindAlternate = { [weak self] in
|
||||
guard let self = self else { return nil }
|
||||
// Create the alternate lazily
|
||||
let alternate = Fiber(
|
||||
bound: alternateScene,
|
||||
state: self.state,
|
||||
subscriptions: self.subscriptions,
|
||||
preferences: self.preferences,
|
||||
layout: self.layout,
|
||||
alternate: self,
|
||||
outputs: self.outputs,
|
||||
typeInfo: self.typeInfo,
|
||||
element: self.element,
|
||||
parent: self.parent?.alternate,
|
||||
elementParent: self.elementParent?.alternate,
|
||||
preferenceParent: self.preferenceParent?.alternate,
|
||||
reconciler: reconciler
|
||||
)
|
||||
self.alternate = alternate
|
||||
if self.parent?.child === self {
|
||||
self.parent?.alternate?.child = alternate // Link it with our parent's alternate.
|
||||
} else {
|
||||
// Find our left sibling.
|
||||
var node = self.parent?.child
|
||||
while node?.sibling !== self {
|
||||
guard node?.sibling != nil else { return alternate }
|
||||
node = node?.sibling
|
||||
}
|
||||
if node?.sibling === self {
|
||||
node?.alternate?.sibling = alternate // Link it with our left sibling's alternate.
|
||||
}
|
||||
}
|
||||
return alternate
|
||||
}
|
||||
}
|
||||
|
||||
init<S: Scene>(
|
||||
bound scene: S,
|
||||
state: [PropertyInfo: MutableStorage],
|
||||
subscriptions: [PropertyInfo: AnyCancellable],
|
||||
preferences: _PreferenceStore?,
|
||||
layout: AnyLayout!,
|
||||
alternate: Fiber,
|
||||
outputs: SceneOutputs,
|
||||
typeInfo: TypeInfo?,
|
||||
element: Renderer.ElementType?,
|
||||
parent: FiberReconciler<Renderer>.Fiber?,
|
||||
elementParent: Fiber?,
|
||||
preferenceParent: Fiber?,
|
||||
reconciler: FiberReconciler<Renderer>?
|
||||
) {
|
||||
self.alternate = alternate
|
||||
self.reconciler = reconciler
|
||||
self.element = element
|
||||
child = nil
|
||||
sibling = nil
|
||||
self.parent = parent
|
||||
self.elementParent = elementParent
|
||||
self.preferenceParent = preferenceParent
|
||||
self.typeInfo = typeInfo
|
||||
self.outputs = outputs
|
||||
self.state = state
|
||||
self.subscriptions = subscriptions
|
||||
self.preferences = preferences
|
||||
if element != nil {
|
||||
self.layout = layout
|
||||
}
|
||||
content = content(for: scene)
|
||||
}
|
||||
|
||||
func update<S: Scene>(
|
||||
with scene: inout S
|
||||
) -> Renderer.ElementType.Content? {
|
||||
typeInfo = TokamakCore.typeInfo(of: S.self)
|
||||
|
||||
let environment = parent?.outputs.environment ?? .init(.init())
|
||||
bindProperties(to: &scene, typeInfo, environment.environment)
|
||||
var updateScene = scene
|
||||
outputs = S._makeScene(.init(
|
||||
content: scene,
|
||||
updateContent: {
|
||||
$0(&updateScene)
|
||||
},
|
||||
environment: environment,
|
||||
traits: .init(),
|
||||
preferenceStore: preferences
|
||||
))
|
||||
scene = updateScene
|
||||
content = content(for: scene)
|
||||
|
||||
if element != nil {
|
||||
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/15/22.
|
||||
//
|
||||
|
||||
/// A reference type that points to a `Renderer`-specific element that has been mounted.
|
||||
/// For instance, a DOM node in the `DOMFiberRenderer`.
|
||||
public protocol FiberElement: AnyObject {
|
||||
associatedtype Content: FiberElementContent
|
||||
var content: Content { get }
|
||||
init(from content: Content)
|
||||
func update(with content: Content)
|
||||
}
|
||||
|
||||
/// The data used to create an `FiberElement`.
|
||||
///
|
||||
/// We re-use `FiberElement` instances in the `Fiber` tree,
|
||||
/// but can re-create and copy `FiberElementContent` as often as needed.
|
||||
public protocol FiberElementContent: Equatable {
|
||||
init<V: View>(from primitiveView: V, useDynamicLayout: Bool)
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FiberReconciler {
|
||||
/// Convert the first level of children of a `View` into a linked list of `Fiber`s.
|
||||
struct TreeReducer: SceneReducer {
|
||||
final class Result {
|
||||
// For references
|
||||
let fiber: Fiber?
|
||||
let visitChildren: (TreeReducer.SceneVisitor) -> ()
|
||||
unowned var parent: Result?
|
||||
var child: Result?
|
||||
var sibling: Result?
|
||||
var newContent: Renderer.ElementType.Content?
|
||||
var elementIndices: [ObjectIdentifier: Int]
|
||||
var nextTraits: _ViewTraitStore
|
||||
|
||||
// For reducing
|
||||
var lastSibling: Result?
|
||||
var nextExisting: Fiber?
|
||||
var nextExistingAlternate: Fiber?
|
||||
|
||||
init(
|
||||
fiber: Fiber?,
|
||||
visitChildren: @escaping (TreeReducer.SceneVisitor) -> (),
|
||||
parent: Result?,
|
||||
child: Fiber?,
|
||||
alternateChild: Fiber?,
|
||||
newContent: Renderer.ElementType.Content? = nil,
|
||||
elementIndices: [ObjectIdentifier: Int],
|
||||
nextTraits: _ViewTraitStore
|
||||
) {
|
||||
self.fiber = fiber
|
||||
self.visitChildren = visitChildren
|
||||
self.parent = parent
|
||||
nextExisting = child
|
||||
nextExistingAlternate = alternateChild
|
||||
self.newContent = newContent
|
||||
self.elementIndices = elementIndices
|
||||
self.nextTraits = nextTraits
|
||||
}
|
||||
}
|
||||
|
||||
static func reduce<S>(into partialResult: inout Result, nextScene: S) where S: Scene {
|
||||
Self.reduce(
|
||||
into: &partialResult,
|
||||
nextValue: nextScene,
|
||||
createFiber: { scene, element, parent, elementParent, preferenceParent, _, _, reconciler in
|
||||
Fiber(
|
||||
&scene,
|
||||
element: element,
|
||||
parent: parent,
|
||||
elementParent: elementParent,
|
||||
preferenceParent: preferenceParent,
|
||||
environment: nil,
|
||||
reconciler: reconciler
|
||||
)
|
||||
},
|
||||
update: { fiber, scene, _, _ in
|
||||
fiber.update(with: &scene)
|
||||
},
|
||||
visitChildren: { $1._visitChildren }
|
||||
)
|
||||
}
|
||||
|
||||
static func reduce<V>(into partialResult: inout Result, nextView: V) where V: View {
|
||||
Self.reduce(
|
||||
into: &partialResult,
|
||||
nextValue: nextView,
|
||||
createFiber: {
|
||||
view, element,
|
||||
parent, elementParent, preferenceParent, elementIndex,
|
||||
traits, reconciler in
|
||||
Fiber(
|
||||
&view,
|
||||
element: element,
|
||||
parent: parent,
|
||||
elementParent: elementParent,
|
||||
preferenceParent: preferenceParent,
|
||||
elementIndex: elementIndex,
|
||||
traits: traits,
|
||||
reconciler: reconciler
|
||||
)
|
||||
},
|
||||
update: { fiber, view, elementIndex, traits in
|
||||
fiber.update(
|
||||
with: &view,
|
||||
elementIndex: elementIndex,
|
||||
traits: fiber.element != nil ? traits : nil
|
||||
)
|
||||
},
|
||||
visitChildren: { reconciler, view in
|
||||
reconciler?.renderer.viewVisitor(for: view) ?? view._visitChildren
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
static func reduce<T>(
|
||||
into partialResult: inout Result,
|
||||
nextValue: T,
|
||||
createFiber: (
|
||||
inout T,
|
||||
Renderer.ElementType?,
|
||||
Fiber?,
|
||||
Fiber?,
|
||||
Fiber?,
|
||||
Int?,
|
||||
_ViewTraitStore,
|
||||
FiberReconciler?
|
||||
) -> Fiber,
|
||||
update: (Fiber, inout T, Int?, _ViewTraitStore) -> Renderer.ElementType.Content?,
|
||||
visitChildren: (FiberReconciler?, T) -> (TreeReducer.SceneVisitor) -> ()
|
||||
) {
|
||||
// Create the node and its element.
|
||||
var nextValue = nextValue
|
||||
|
||||
let resultChild: Result
|
||||
if let existing = partialResult.nextExisting {
|
||||
// If a fiber already exists, simply update it with the new view.
|
||||
let key: ObjectIdentifier?
|
||||
if let elementParent = existing.elementParent {
|
||||
key = ObjectIdentifier(elementParent)
|
||||
} else {
|
||||
key = nil
|
||||
}
|
||||
let newContent = update(
|
||||
existing,
|
||||
&nextValue,
|
||||
key.map { partialResult.elementIndices[$0, default: 0] },
|
||||
partialResult.nextTraits
|
||||
)
|
||||
resultChild = Result(
|
||||
fiber: existing,
|
||||
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
|
||||
parent: partialResult,
|
||||
child: existing.child,
|
||||
alternateChild: existing.alternate?.child,
|
||||
newContent: newContent,
|
||||
elementIndices: partialResult.elementIndices,
|
||||
nextTraits: existing.element != nil ? .init() : partialResult.nextTraits
|
||||
)
|
||||
partialResult.nextExisting = existing.sibling
|
||||
partialResult.nextExistingAlternate = partialResult.nextExistingAlternate?.sibling
|
||||
} else {
|
||||
let elementParent = partialResult.fiber?.element != nil
|
||||
? partialResult.fiber
|
||||
: partialResult.fiber?.elementParent
|
||||
let preferenceParent = partialResult.fiber?.preferences != nil
|
||||
? partialResult.fiber
|
||||
: partialResult.fiber?.preferenceParent
|
||||
let key: ObjectIdentifier?
|
||||
if let elementParent = elementParent {
|
||||
key = ObjectIdentifier(elementParent)
|
||||
} else {
|
||||
key = nil
|
||||
}
|
||||
// Otherwise, create a new fiber for this child.
|
||||
let fiber = createFiber(
|
||||
&nextValue,
|
||||
partialResult.nextExistingAlternate?.element,
|
||||
partialResult.fiber,
|
||||
elementParent,
|
||||
preferenceParent,
|
||||
key.map { partialResult.elementIndices[$0, default: 0] },
|
||||
partialResult.nextTraits,
|
||||
partialResult.fiber?.reconciler
|
||||
)
|
||||
|
||||
// If a fiber already exists for an alternate, link them.
|
||||
if let alternate = partialResult.nextExistingAlternate {
|
||||
fiber.alternate = alternate
|
||||
partialResult.nextExistingAlternate = alternate.sibling
|
||||
}
|
||||
resultChild = Result(
|
||||
fiber: fiber,
|
||||
visitChildren: visitChildren(partialResult.fiber?.reconciler, nextValue),
|
||||
parent: partialResult,
|
||||
child: nil,
|
||||
alternateChild: fiber.alternate?.child,
|
||||
elementIndices: partialResult.elementIndices,
|
||||
nextTraits: fiber.element != nil ? .init() : partialResult.nextTraits
|
||||
)
|
||||
}
|
||||
// Get the last child element we've processed, and add the new child as its sibling.
|
||||
if let lastSibling = partialResult.lastSibling {
|
||||
lastSibling.fiber?.sibling = resultChild.fiber
|
||||
lastSibling.sibling = resultChild
|
||||
} else {
|
||||
// Otherwise setup the first child
|
||||
partialResult.fiber?.child = resultChild.fiber
|
||||
partialResult.child = resultChild
|
||||
}
|
||||
partialResult.lastSibling = resultChild
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombineShim
|
||||
|
||||
/// A reconciler modeled after React's
|
||||
/// [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
||||
public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||
/// The root node in the `Fiber` tree that represents the `View`s currently rendered on screen.
|
||||
@_spi(TokamakCore)
|
||||
public var current: Fiber!
|
||||
|
||||
/// The alternate of `current`, or the work in progress tree root.
|
||||
///
|
||||
/// We must keep a strong reference to both the current and alternate tree roots,
|
||||
/// as they only keep weak references to each other.
|
||||
private var alternate: Fiber!
|
||||
|
||||
/// The `FiberRenderer` used to create and update the `Element`s on screen.
|
||||
public let renderer: Renderer
|
||||
|
||||
/// Enabled passes to run on each `reconcile(from:)` call.
|
||||
private let passes: [FiberReconcilerPass]
|
||||
|
||||
private let caches: Caches
|
||||
|
||||
private var sceneSizeCancellable: AnyCancellable?
|
||||
|
||||
private var isReconciling = false
|
||||
/// The identifiers for each `Fiber` that changed state during the last run loop.
|
||||
///
|
||||
/// The reconciler loop starts at the root of the `View` hierarchy
|
||||
/// to ensure all preference values are passed down correctly.
|
||||
/// To help mitigate performance issues related to this, we only perform reconcile
|
||||
/// checks when we reach a changed `Fiber`.
|
||||
private var changedFibers = Set<ObjectIdentifier>()
|
||||
public var afterReconcileActions = [() -> ()]()
|
||||
|
||||
struct RootView<Content: View>: View {
|
||||
let content: Content
|
||||
let reconciler: FiberReconciler<Renderer>
|
||||
|
||||
var environment: EnvironmentValues {
|
||||
var environment = reconciler.renderer.defaultEnvironment
|
||||
environment.measureText = reconciler.renderer.measureText
|
||||
environment.measureImage = reconciler.renderer.measureImage
|
||||
environment.afterReconcile = reconciler.afterReconcile
|
||||
return environment
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
RootLayout(renderer: reconciler.renderer).callAsFunction {
|
||||
content
|
||||
.environmentValues(environment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Layout` container for the root of a `View` hierarchy.
|
||||
///
|
||||
/// Simply places each `View` in the center of its bounds.
|
||||
struct RootLayout: Layout {
|
||||
let renderer: Renderer
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) -> CGSize {
|
||||
renderer.sceneSize.value
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
for subview in subviews {
|
||||
subview.place(
|
||||
at: .init(x: bounds.midX, y: bounds.midY),
|
||||
anchor: .center,
|
||||
proposal: .init(width: bounds.width, height: bounds.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init<V: View>(_ renderer: Renderer, _ view: V) {
|
||||
self.renderer = renderer
|
||||
if renderer.useDynamicLayout {
|
||||
passes = [.reconcile, .layout]
|
||||
} else {
|
||||
passes = [.reconcile]
|
||||
}
|
||||
caches = Caches()
|
||||
var view = RootView(content: view, reconciler: self)
|
||||
current = .init(
|
||||
&view,
|
||||
element: renderer.rootElement,
|
||||
parent: nil,
|
||||
elementParent: nil,
|
||||
preferenceParent: nil,
|
||||
elementIndex: 0,
|
||||
traits: nil,
|
||||
reconciler: self
|
||||
)
|
||||
// Start by building the initial tree.
|
||||
alternate = current.createAndBindAlternate?()
|
||||
|
||||
sceneSizeCancellable = renderer.sceneSize.removeDuplicates().sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.fiberChanged(self.current)
|
||||
}
|
||||
}
|
||||
|
||||
public init<A: App>(_ renderer: Renderer, _ app: A) {
|
||||
self.renderer = renderer
|
||||
if renderer.useDynamicLayout {
|
||||
passes = [.reconcile, .layout]
|
||||
} else {
|
||||
passes = [.reconcile]
|
||||
}
|
||||
caches = Caches()
|
||||
var environment = renderer.defaultEnvironment
|
||||
environment.measureText = renderer.measureText
|
||||
environment.measureImage = renderer.measureImage
|
||||
environment.afterReconcile = afterReconcile
|
||||
var app = app
|
||||
current = .init(
|
||||
&app,
|
||||
rootElement: renderer.rootElement,
|
||||
rootEnvironment: environment,
|
||||
reconciler: self
|
||||
)
|
||||
// Start by building the initial tree.
|
||||
alternate = current.createAndBindAlternate?()
|
||||
|
||||
sceneSizeCancellable = renderer.sceneSize.removeDuplicates().sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.fiberChanged(self.current)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visitor that performs each pass used by the `FiberReconciler`.
|
||||
final class ReconcilerVisitor: AppVisitor, SceneVisitor, ViewVisitor {
|
||||
let root: Fiber
|
||||
/// Any `Fiber`s that changed state during the last run loop.
|
||||
let changedFibers: Set<ObjectIdentifier>
|
||||
unowned let reconciler: FiberReconciler
|
||||
var mutations = [Mutation<Renderer>]()
|
||||
|
||||
init(root: Fiber, changedFibers: Set<ObjectIdentifier>, reconciler: FiberReconciler) {
|
||||
self.root = root
|
||||
self.changedFibers = changedFibers
|
||||
self.reconciler = reconciler
|
||||
}
|
||||
|
||||
func visit<A>(_ app: A) where A: App {
|
||||
visitAny(app) { $0.visit(app.body) }
|
||||
}
|
||||
|
||||
func visit<S>(_ scene: S) where S: Scene {
|
||||
visitAny(scene, scene._visitChildren)
|
||||
}
|
||||
|
||||
func visit<V>(_ view: V) where V: View {
|
||||
visitAny(view, reconciler.renderer.viewVisitor(for: view))
|
||||
}
|
||||
|
||||
private func visitAny(
|
||||
_ content: Any,
|
||||
_ visitChildren: @escaping (TreeReducer.SceneVisitor) -> ()
|
||||
) {
|
||||
let alternateRoot: Fiber?
|
||||
if let alternate = root.alternate {
|
||||
alternateRoot = alternate
|
||||
} else {
|
||||
alternateRoot = root.createAndBindAlternate?()
|
||||
}
|
||||
let rootResult = TreeReducer.Result(
|
||||
fiber: alternateRoot, // The alternate is the WIP node.
|
||||
visitChildren: visitChildren,
|
||||
parent: nil,
|
||||
child: alternateRoot?.child,
|
||||
alternateChild: root.child,
|
||||
elementIndices: [:],
|
||||
nextTraits: .init()
|
||||
)
|
||||
reconciler.caches.clear()
|
||||
for pass in reconciler.passes {
|
||||
pass.run(
|
||||
in: reconciler,
|
||||
root: rootResult,
|
||||
changedFibers: changedFibers,
|
||||
caches: reconciler.caches
|
||||
)
|
||||
}
|
||||
mutations = reconciler.caches.mutations
|
||||
}
|
||||
}
|
||||
|
||||
func afterReconcile(_ action: @escaping () -> ()) {
|
||||
guard isReconciling == true
|
||||
else {
|
||||
action()
|
||||
return
|
||||
}
|
||||
afterReconcileActions.append(action)
|
||||
}
|
||||
|
||||
/// Called by any `Fiber` that experiences a state change.
|
||||
///
|
||||
/// Reconciliation only runs after every change during the current run loop has been performed.
|
||||
func fiberChanged(_ fiber: Fiber) {
|
||||
guard let alternate = fiber.alternate ?? fiber.createAndBindAlternate?()
|
||||
else { return }
|
||||
let shouldSchedule = changedFibers.isEmpty
|
||||
changedFibers.insert(ObjectIdentifier(alternate))
|
||||
if shouldSchedule {
|
||||
renderer.schedule { [weak self] in
|
||||
self?.reconcile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform each `FiberReconcilerPass` given the `changedFibers`.
|
||||
///
|
||||
/// A `reconcile()` call is queued from `fiberChanged` once per run loop.
|
||||
func reconcile() {
|
||||
isReconciling = true
|
||||
let changedFibers = changedFibers
|
||||
self.changedFibers.removeAll()
|
||||
// Create a list of mutations.
|
||||
let visitor = ReconcilerVisitor(root: current, changedFibers: changedFibers, reconciler: self)
|
||||
switch current.content {
|
||||
case let .view(_, visit):
|
||||
visit(visitor)
|
||||
case let .scene(_, visit):
|
||||
visit(visitor)
|
||||
case let .app(_, visit):
|
||||
visit(visitor)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
// Apply mutations to the rendered output.
|
||||
renderer.commit(visitor.mutations)
|
||||
|
||||
// Swap the root out for its alternate.
|
||||
// Essentially, making the work in progress tree the current,
|
||||
// and leaving the current available to be the work in progress
|
||||
// on our next update.
|
||||
let alternate = alternate
|
||||
self.alternate = current
|
||||
current = alternate
|
||||
|
||||
isReconciling = false
|
||||
|
||||
for action in afterReconcileActions {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
private enum AfterReconcileKey: EnvironmentKey {
|
||||
static let defaultValue: (@escaping () -> ()) -> () = { _ in }
|
||||
}
|
||||
|
||||
var afterReconcile: (@escaping () -> ()) -> () {
|
||||
get { self[AfterReconcileKey.self] }
|
||||
set { self[AfterReconcileKey.self] = newValue }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OpenCombineShim
|
||||
|
||||
/// A renderer capable of performing mutations specified by a `FiberReconciler`.
|
||||
public protocol FiberRenderer {
|
||||
/// The element class this renderer uses.
|
||||
associatedtype ElementType: FiberElement
|
||||
|
||||
/// Check whether a `View` is a primitive for this renderer.
|
||||
static func isPrimitive<V>(_ view: V) -> Bool where V: View
|
||||
|
||||
/// Override the default `_visitChildren` implementation for a primitive `View`.
|
||||
func visitPrimitiveChildren<Primitive, Visitor>(
|
||||
_ view: Primitive
|
||||
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor
|
||||
|
||||
/// Apply the mutations to the elements.
|
||||
func commit(_ mutations: [Mutation<Self>])
|
||||
|
||||
/// The root element all top level views should be mounted on.
|
||||
var rootElement: ElementType { get }
|
||||
|
||||
/// The smallest set of initial `EnvironmentValues` needed for this renderer to function.
|
||||
var defaultEnvironment: EnvironmentValues { get }
|
||||
|
||||
/// The size of the window we are rendering in.
|
||||
///
|
||||
/// Layout is automatically updated whenever the size changes.
|
||||
var sceneSize: CurrentValueSubject<CGSize, Never> { get }
|
||||
|
||||
/// Whether layout is enabled for this renderer.
|
||||
var useDynamicLayout: Bool { get }
|
||||
|
||||
/// Calculate the size of `Text` in `environment` for layout.
|
||||
func measureText(
|
||||
_ text: Text,
|
||||
proposal: ProposedViewSize,
|
||||
in environment: EnvironmentValues
|
||||
) -> CGSize
|
||||
|
||||
/// Calculate the size of an `Image` in `environment` for layout.
|
||||
func measureImage(
|
||||
_ image: Image,
|
||||
proposal: ProposedViewSize,
|
||||
in environment: EnvironmentValues
|
||||
) -> CGSize
|
||||
|
||||
/// Run `action` on the next run loop.
|
||||
///
|
||||
/// Called by the `FiberReconciler` to perform reconciliation after all changed Fibers are collected.
|
||||
///
|
||||
/// For example, take the following sample `View`:
|
||||
///
|
||||
/// struct DuelOfTheStates: View {
|
||||
/// @State private var hits1 = 0
|
||||
/// @State private var hits2 = 0
|
||||
///
|
||||
/// var body: some View {
|
||||
/// Button("Hit") {
|
||||
/// hits1 += 1
|
||||
/// hits2 += 2
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// When the button is pressed, both `hits1` and `hits2` are updated.
|
||||
/// If reconciliation was done on every state change, we would needlessly run it twice,
|
||||
/// once for `hits1` and again for `hits2`.
|
||||
///
|
||||
/// Instead, we create a list of changed fibers
|
||||
/// (in this case just `DuelOfTheStates` as both properties were on it),
|
||||
/// and reconcile after all changes have been collected.
|
||||
func schedule(_ action: @escaping () -> ())
|
||||
}
|
||||
|
||||
public extension FiberRenderer {
|
||||
var defaultEnvironment: EnvironmentValues { .init() }
|
||||
|
||||
func visitPrimitiveChildren<Primitive, Visitor>(
|
||||
_ view: Primitive
|
||||
) -> ViewVisitorF<Visitor>? where Primitive: View, Visitor: ViewVisitor {
|
||||
nil
|
||||
}
|
||||
|
||||
func viewVisitor<V: View, Visitor: ViewVisitor>(for view: V) -> ViewVisitorF<Visitor> {
|
||||
if Self.isPrimitive(view) {
|
||||
return visitPrimitiveChildren(view) ?? view._visitChildren
|
||||
} else {
|
||||
return view._visitChildren
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@_disfavoredOverload
|
||||
func render<V: View>(_ view: V) -> FiberReconciler<Self> {
|
||||
.init(self, view)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@_disfavoredOverload
|
||||
func render<A: App>(_ app: A) -> FiberReconciler<Self> {
|
||||
.init(self, app)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
private enum MeasureTextKey: EnvironmentKey {
|
||||
static var defaultValue: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
{ _, _, _ in .zero }
|
||||
}
|
||||
}
|
||||
|
||||
var measureText: (Text, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
get { self[MeasureTextKey.self] }
|
||||
set { self[MeasureTextKey.self] = newValue }
|
||||
}
|
||||
|
||||
private enum MeasureImageKey: EnvironmentKey {
|
||||
static var defaultValue: (Image, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
{ _, _, _ in .zero }
|
||||
}
|
||||
}
|
||||
|
||||
var measureImage: (Image, ProposedViewSize, EnvironmentValues) -> CGSize {
|
||||
get { self[MeasureImageKey.self] }
|
||||
set { self[MeasureImageKey.self] = newValue }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The cache for a `ContainedZLayout`.
|
||||
@_spi(TokamakCore)
|
||||
public struct ContainedZLayoutCache {
|
||||
/// The result of `dimensions(in:)` for the primary subview.
|
||||
var primaryDimensions: ViewDimensions?
|
||||
}
|
||||
|
||||
/// A layout that fits secondary subviews to the size of a primary subview.
|
||||
///
|
||||
/// Used to implement `_BackgroundLayout` and `_OverlayLayout`.
|
||||
@_spi(TokamakCore)
|
||||
public protocol ContainedZLayout: Layout where Cache == ContainedZLayoutCache {
|
||||
var alignment: Alignment { get }
|
||||
/// An accessor for the primary subview from a `LayoutSubviews` collection.
|
||||
static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { get }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public extension ContainedZLayout {
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
.init()
|
||||
}
|
||||
|
||||
func spacing(subviews: LayoutSubviews, cache: inout Cache) -> ViewSpacing {
|
||||
subviews[keyPath: Self.primarySubview]?.spacing ?? .init()
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
// Assume the dimensions of the primary subview.
|
||||
cache.primaryDimensions = subviews[keyPath: Self.primarySubview]?.dimensions(in: proposal)
|
||||
return .init(
|
||||
width: cache.primaryDimensions?.width ?? .zero,
|
||||
height: cache.primaryDimensions?.height ?? .zero
|
||||
)
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
let proposal = ProposedViewSize(bounds.size)
|
||||
|
||||
// Place the foreground at the origin.
|
||||
subviews[keyPath: Self.primarySubview]?.place(at: bounds.origin, proposal: proposal)
|
||||
|
||||
let backgroundSubviews = subviews[keyPath: Self.primarySubview] == subviews.first
|
||||
? subviews.dropFirst(1)
|
||||
: subviews.dropLast(1)
|
||||
|
||||
/// The `ViewDimensions` of the subview with the greatest `width`, used to follow `alignment`.
|
||||
var widest: ViewDimensions?
|
||||
/// The `ViewDimensions` of the subview with the greatest `height`.
|
||||
var tallest: ViewDimensions?
|
||||
|
||||
let dimensions = backgroundSubviews.map { subview -> ViewDimensions in
|
||||
let dimensions = subview.dimensions(in: proposal)
|
||||
if dimensions.width > (widest?.width ?? .zero) {
|
||||
widest = dimensions
|
||||
}
|
||||
if dimensions.height > (tallest?.height ?? .zero) {
|
||||
tallest = dimensions
|
||||
}
|
||||
return dimensions
|
||||
}
|
||||
|
||||
/// The alignment guide values of the primary subview.
|
||||
let primaryOffset = CGSize(
|
||||
width: cache.primaryDimensions?[alignment.horizontal] ?? .zero,
|
||||
height: cache.primaryDimensions?[alignment.vertical] ?? .zero
|
||||
)
|
||||
/// The alignment guide values of the secondary subviews (background/overlay).
|
||||
/// Uses the widest/tallest element to get the full extents.
|
||||
let secondaryOffset = CGSize(
|
||||
width: widest?[alignment.horizontal] ?? .zero,
|
||||
height: tallest?[alignment.vertical] ?? .zero
|
||||
)
|
||||
/// The center offset of the secondary subviews.
|
||||
let secondaryCenter = CGSize(
|
||||
width: widest?[HorizontalAlignment.center] ?? .zero,
|
||||
height: tallest?[VerticalAlignment.center] ?? .zero
|
||||
)
|
||||
/// The origin of the secondary subviews with alignment.
|
||||
let secondaryOrigin = CGPoint(
|
||||
x: bounds.minX + primaryOffset.width - secondaryOffset.width + secondaryCenter.width,
|
||||
y: bounds.minY + primaryOffset.height - secondaryOffset.height + secondaryCenter.height
|
||||
)
|
||||
for (index, subview) in backgroundSubviews.enumerated() {
|
||||
// Background elements are centered between each other, but placed with `alignment`
|
||||
// all together on the foreground.
|
||||
subview.place(
|
||||
at: .init(
|
||||
x: secondaryOrigin.x - dimensions[index][HorizontalAlignment.center],
|
||||
y: secondaryOrigin.y - dimensions[index][VerticalAlignment.center]
|
||||
),
|
||||
proposal: proposal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expects the primary subview to be last.
|
||||
@_spi(TokamakCore)
|
||||
extension _BackgroundLayout: ContainedZLayout {
|
||||
public static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { \.last }
|
||||
}
|
||||
|
||||
/// Expects the primary subview to be the first.
|
||||
@_spi(TokamakCore)
|
||||
extension _OverlayLayout: ContainedZLayout {
|
||||
public static var primarySubview: KeyPath<LayoutSubviews, LayoutSubview?> { \.first }
|
||||
}
|
|
@ -0,0 +1,477 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Erase a `Layout` conformance to an `AnyLayout`.
|
||||
///
|
||||
/// This could potentially be removed in Swift 5.7 in favor of `any Layout`.
|
||||
public protocol _AnyLayout {
|
||||
func _erased() -> AnyLayout
|
||||
}
|
||||
|
||||
/// A type that participates in the layout pass.
|
||||
///
|
||||
/// Any `View` or `Scene` that implements this protocol will be used to compute layout in
|
||||
/// a `FiberRenderer` with `useDynamicLayout` set to `true`.
|
||||
public protocol Layout: Animatable, _AnyLayout {
|
||||
static var layoutProperties: LayoutProperties { get }
|
||||
|
||||
associatedtype Cache = ()
|
||||
|
||||
/// Proxies for the children of this container.
|
||||
typealias Subviews = LayoutSubviews
|
||||
|
||||
/// Create a fresh `Cache`. Use it to store complex operations,
|
||||
/// or to pass data between `sizeThatFits` and `placeSubviews`.
|
||||
///
|
||||
/// - Note: There are no guarantees about when the cache will be recreated,
|
||||
/// and the behavior could change at any time.
|
||||
func makeCache(subviews: Self.Subviews) -> Self.Cache
|
||||
|
||||
/// Update the existing `Cache` before each layout pass.
|
||||
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)
|
||||
|
||||
/// The preferred spacing for this `View` and its subviews.
|
||||
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing
|
||||
|
||||
/// Request a size to contain the subviews and fit within `proposal`.
|
||||
/// If you provide a size that does not fit within `proposal`, the parent will still respect it.
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGSize
|
||||
|
||||
/// Place each subview with `LayoutSubview.place(at:anchor:proposal:)`.
|
||||
///
|
||||
/// - Note: The bounds are not necessarily at `(0, 0)`, so use `bounds.minX` and `bounds.minY`
|
||||
/// to correctly position relative to the container.
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
)
|
||||
|
||||
/// Override the value of a `HorizontalAlignment` value.
|
||||
func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat?
|
||||
|
||||
/// Override the value of a `VerticalAlignment` value.
|
||||
func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat?
|
||||
}
|
||||
|
||||
public extension Layout {
|
||||
func _erased() -> AnyLayout {
|
||||
.init(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Layout where Self.Cache == () {
|
||||
func makeCache(subviews: Self.Subviews) -> Self.Cache {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
public extension Layout {
|
||||
static var layoutProperties: LayoutProperties {
|
||||
.init()
|
||||
}
|
||||
|
||||
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews) {
|
||||
cache = makeCache(subviews: subviews)
|
||||
}
|
||||
|
||||
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing {
|
||||
subviews.reduce(
|
||||
into: subviews.first.map {
|
||||
.init(
|
||||
viewType: $0.spacing.viewType,
|
||||
top: { _ in 0 },
|
||||
leading: { _ in 0 },
|
||||
bottom: { _ in 0 },
|
||||
trailing: { _ in 0 }
|
||||
)
|
||||
} ?? .zero
|
||||
) { $0.formUnion($1.spacing) }
|
||||
}
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat? {
|
||||
nil
|
||||
}
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension Layout {
|
||||
/// Render `content` using `self` as the layout container.
|
||||
func callAsFunction<V>(@ViewBuilder _ content: () -> V) -> some View where V: View {
|
||||
LayoutView(layout: self, content: content())
|
||||
}
|
||||
}
|
||||
|
||||
/// A `View` that renders its children with a `Layout`.
|
||||
@_spi(TokamakCore)
|
||||
public struct LayoutView<L: Layout, Content: View>: View, Layout {
|
||||
let layout: L
|
||||
let content: Content
|
||||
|
||||
public typealias Cache = L.Cache
|
||||
|
||||
public func makeCache(subviews: Subviews) -> L.Cache {
|
||||
layout.makeCache(subviews: subviews)
|
||||
}
|
||||
|
||||
public func updateCache(_ cache: inout L.Cache, subviews: Subviews) {
|
||||
layout.updateCache(&cache, subviews: subviews)
|
||||
}
|
||||
|
||||
public func spacing(subviews: Subviews, cache: inout L.Cache) -> ViewSpacing {
|
||||
layout.spacing(subviews: subviews, cache: &cache)
|
||||
}
|
||||
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
layout.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &cache)
|
||||
}
|
||||
|
||||
public func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout L.Cache
|
||||
) -> CGFloat? {
|
||||
layout.explicitAlignment(
|
||||
of: guide, in: bounds, proposal: proposal, subviews: subviews, cache: &cache
|
||||
)
|
||||
}
|
||||
|
||||
public func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout L.Cache
|
||||
) -> CGFloat? {
|
||||
layout.explicitAlignment(
|
||||
of: guide, in: bounds, proposal: proposal, subviews: subviews, cache: &cache
|
||||
)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
/// A default `Layout` that fits to the first subview and places its children at its origin.
|
||||
struct DefaultLayout: Layout {
|
||||
/// An erased `DefaultLayout` that is shared between all views.
|
||||
static let shared: AnyLayout = .init(Self())
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
let size = subviews.first?.sizeThatFits(proposal) ?? .zero
|
||||
return size
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
for subview in subviews {
|
||||
subview.place(at: bounds.origin, proposal: proposal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a container for an erased `Layout` type.
|
||||
///
|
||||
/// Matches the `Layout` protocol with `Cache` erased to `Any`.
|
||||
@usableFromInline
|
||||
protocol AnyLayoutBox: AnyObject {
|
||||
var layoutProperties: LayoutProperties { get }
|
||||
|
||||
typealias Subviews = LayoutSubviews
|
||||
typealias Cache = Any
|
||||
|
||||
func makeCache(subviews: Self.Subviews) -> Self.Cache
|
||||
|
||||
func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)
|
||||
|
||||
func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGSize
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
)
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat?
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Self.Subviews,
|
||||
cache: inout Self.Cache
|
||||
) -> CGFloat?
|
||||
|
||||
var animatableData: _AnyAnimatableData { get set }
|
||||
}
|
||||
|
||||
final class ConcreteLayoutBox<L: Layout>: AnyLayoutBox {
|
||||
var base: L
|
||||
|
||||
init(_ base: L) {
|
||||
self.base = base
|
||||
}
|
||||
|
||||
var layoutProperties: LayoutProperties { L.layoutProperties }
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
base.makeCache(subviews: subviews)
|
||||
}
|
||||
|
||||
private func typedCache<R>(
|
||||
subviews: Subviews,
|
||||
erasedCache: inout Cache,
|
||||
_ action: (inout L.Cache) -> R
|
||||
) -> R {
|
||||
var typedCache = erasedCache as? L.Cache ?? base.makeCache(subviews: subviews)
|
||||
defer { erasedCache = typedCache }
|
||||
return action(&typedCache)
|
||||
}
|
||||
|
||||
func updateCache(_ cache: inout Cache, subviews: Subviews) {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.updateCache(&$0, subviews: subviews)
|
||||
}
|
||||
}
|
||||
|
||||
func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.spacing(subviews: subviews, cache: &$0)
|
||||
}
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.sizeThatFits(proposal: proposal, subviews: subviews, cache: &$0)
|
||||
}
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &$0)
|
||||
}
|
||||
}
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGFloat? {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.explicitAlignment(
|
||||
of: guide,
|
||||
in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &$0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGFloat? {
|
||||
typedCache(subviews: subviews, erasedCache: &cache) {
|
||||
base.explicitAlignment(
|
||||
of: guide,
|
||||
in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &$0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var animatableData: _AnyAnimatableData {
|
||||
get {
|
||||
.init(base.animatableData)
|
||||
}
|
||||
set {
|
||||
guard let newData = newValue.value as? L.AnimatableData else { return }
|
||||
base.animatableData = newData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct AnyLayout: Layout {
|
||||
var storage: AnyLayoutBox
|
||||
|
||||
public init<L>(_ layout: L) where L: Layout {
|
||||
storage = ConcreteLayoutBox(layout)
|
||||
}
|
||||
|
||||
public struct Cache {
|
||||
var erasedCache: Any
|
||||
}
|
||||
|
||||
public func makeCache(subviews: AnyLayout.Subviews) -> AnyLayout.Cache {
|
||||
.init(erasedCache: storage.makeCache(subviews: subviews))
|
||||
}
|
||||
|
||||
public func updateCache(_ cache: inout AnyLayout.Cache, subviews: AnyLayout.Subviews) {
|
||||
storage.updateCache(&cache.erasedCache, subviews: subviews)
|
||||
}
|
||||
|
||||
public func spacing(subviews: AnyLayout.Subviews, cache: inout AnyLayout.Cache) -> ViewSpacing {
|
||||
storage.spacing(subviews: subviews, cache: &cache.erasedCache)
|
||||
}
|
||||
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: AnyLayout.Subviews,
|
||||
cache: inout AnyLayout.Cache
|
||||
) -> CGSize {
|
||||
storage.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache.erasedCache)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: AnyLayout.Subviews,
|
||||
cache: inout AnyLayout.Cache
|
||||
) {
|
||||
storage.placeSubviews(
|
||||
in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &cache.erasedCache
|
||||
)
|
||||
}
|
||||
|
||||
public func explicitAlignment(
|
||||
of guide: HorizontalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: AnyLayout.Subviews,
|
||||
cache: inout AnyLayout.Cache
|
||||
) -> CGFloat? {
|
||||
storage.explicitAlignment(
|
||||
of: guide,
|
||||
in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &cache.erasedCache
|
||||
)
|
||||
}
|
||||
|
||||
public func explicitAlignment(
|
||||
of guide: VerticalAlignment,
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: AnyLayout.Subviews,
|
||||
cache: inout AnyLayout.Cache
|
||||
) -> CGFloat? {
|
||||
storage.explicitAlignment(
|
||||
of: guide, in: bounds,
|
||||
proposal: proposal,
|
||||
subviews: subviews,
|
||||
cache: &cache.erasedCache
|
||||
)
|
||||
}
|
||||
|
||||
public var animatableData: _AnyAnimatableData {
|
||||
get {
|
||||
_AnyAnimatableData(storage.animatableData)
|
||||
}
|
||||
set {
|
||||
storage.animatableData = newValue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/22/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@usableFromInline
|
||||
enum LayoutPriorityTraitKey: _ViewTraitKey {
|
||||
@inlinable
|
||||
static var defaultValue: Double { 0 }
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func layoutPriority(_ value: Double) -> some View {
|
||||
_trait(LayoutPriorityTraitKey.self, value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
/// Metadata about a `Layout`.
|
||||
public struct LayoutProperties {
|
||||
public var stackOrientation: Axis?
|
||||
|
||||
public init() {
|
||||
stackOrientation = nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A collection of `LayoutSubview` proxies.
|
||||
public struct LayoutSubviews: Equatable, RandomAccessCollection {
|
||||
public var layoutDirection: LayoutDirection
|
||||
var storage: [LayoutSubview]
|
||||
|
||||
init(layoutDirection: LayoutDirection, storage: [LayoutSubview]) {
|
||||
self.layoutDirection = layoutDirection
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
init<R: FiberRenderer>(_ node: FiberReconciler<R>.Fiber) {
|
||||
self.init(
|
||||
layoutDirection: node.outputs.environment.environment.layoutDirection,
|
||||
storage: []
|
||||
)
|
||||
}
|
||||
|
||||
public typealias SubSequence = LayoutSubviews
|
||||
public typealias Element = LayoutSubview
|
||||
public typealias Index = Int
|
||||
public typealias Indices = Range<LayoutSubviews.Index>
|
||||
public typealias Iterator = IndexingIterator<LayoutSubviews>
|
||||
|
||||
public var startIndex: Int {
|
||||
storage.startIndex
|
||||
}
|
||||
|
||||
public var endIndex: Int {
|
||||
storage.endIndex
|
||||
}
|
||||
|
||||
public subscript(index: Int) -> LayoutSubviews.Element {
|
||||
storage[index]
|
||||
}
|
||||
|
||||
public subscript(bounds: Range<Int>) -> LayoutSubviews {
|
||||
.init(layoutDirection: layoutDirection, storage: .init(storage[bounds]))
|
||||
}
|
||||
|
||||
public subscript<S>(indices: S) -> LayoutSubviews where S: Sequence, S.Element == Int {
|
||||
.init(
|
||||
layoutDirection: layoutDirection,
|
||||
storage: storage.enumerated()
|
||||
.filter { indices.contains($0.offset) }
|
||||
.map(\.element)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A proxy representing a child of a `Layout`.
|
||||
///
|
||||
/// Access size requests, alignment guide values, spacing preferences, and any layout values using
|
||||
/// this proxy.
|
||||
///
|
||||
/// `Layout` types are expected to call `place(at:anchor:proposal:)` on all subviews.
|
||||
/// If `place(at:anchor:proposal:)` is not called, the center will be used as its position.
|
||||
public struct LayoutSubview: Equatable {
|
||||
private let id: ObjectIdentifier
|
||||
private let storage: AnyStorage
|
||||
|
||||
/// A protocol used to erase `Storage<R>`.
|
||||
private class AnyStorage {
|
||||
let traits: _ViewTraitStore?
|
||||
|
||||
init(traits: _ViewTraitStore?) {
|
||||
self.traits = traits
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
||||
fatalError("Implement \(#function) in subclass")
|
||||
}
|
||||
|
||||
func dimensions(_ sizeThatFits: CGSize) -> ViewDimensions {
|
||||
fatalError("Implement \(#function) in subclass")
|
||||
}
|
||||
|
||||
func place(
|
||||
_ proposal: ProposedViewSize,
|
||||
_ dimensions: ViewDimensions,
|
||||
_ position: CGPoint,
|
||||
_ anchor: UnitPoint
|
||||
) {
|
||||
fatalError("Implement \(#function) in subclass")
|
||||
}
|
||||
|
||||
func spacing() -> ViewSpacing {
|
||||
fatalError("Implement \(#function) in subclass")
|
||||
}
|
||||
}
|
||||
|
||||
/// The backing storage for a `LayoutSubview`. This contains the underlying implementations for
|
||||
/// methods accessing the `fiber`, `element`, and `cache` this subview represents.
|
||||
private final class Storage<R: FiberRenderer>: AnyStorage {
|
||||
weak var fiber: FiberReconciler<R>.Fiber?
|
||||
weak var element: R.ElementType?
|
||||
unowned var caches: FiberReconciler<R>.Caches
|
||||
|
||||
init(
|
||||
traits: _ViewTraitStore?,
|
||||
fiber: FiberReconciler<R>.Fiber?,
|
||||
element: R.ElementType?,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) {
|
||||
self.fiber = fiber
|
||||
self.element = element
|
||||
self.caches = caches
|
||||
super.init(traits: traits)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
||||
guard let fiber = fiber else { return .zero }
|
||||
let request = FiberReconciler<R>.Caches.LayoutCache.SizeThatFitsRequest(proposal)
|
||||
return caches.updateLayoutCache(for: fiber) { cache in
|
||||
guard let layout = fiber.layout else { return .zero }
|
||||
if let size = cache.sizeThatFits[request] {
|
||||
return size
|
||||
} else {
|
||||
let size = layout.sizeThatFits(
|
||||
proposal: proposal,
|
||||
subviews: caches.layoutSubviews(for: fiber),
|
||||
cache: &cache.cache
|
||||
)
|
||||
cache.sizeThatFits[request] = size
|
||||
if let alternate = fiber.alternate {
|
||||
caches.updateLayoutCache(for: alternate) { alternateCache in
|
||||
alternateCache.cache = cache.cache
|
||||
alternateCache.sizeThatFits[request] = size
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
} ?? .zero
|
||||
}
|
||||
|
||||
override func dimensions(_ sizeThatFits: CGSize) -> ViewDimensions {
|
||||
// TODO: Add `alignmentGuide` modifier and pass into `ViewDimensions`
|
||||
ViewDimensions(size: sizeThatFits, alignmentGuides: [:])
|
||||
}
|
||||
|
||||
override func place(
|
||||
_ proposal: ProposedViewSize,
|
||||
_ dimensions: ViewDimensions,
|
||||
_ position: CGPoint,
|
||||
_ anchor: UnitPoint
|
||||
) {
|
||||
guard let fiber = fiber, let element = element else { return }
|
||||
let geometry = ViewGeometry(
|
||||
// Shift to the anchor point in the parent's coordinate space.
|
||||
origin: .init(origin: .init(
|
||||
x: position.x - (dimensions.width * anchor.x),
|
||||
y: position.y - (dimensions.height * anchor.y)
|
||||
)),
|
||||
dimensions: dimensions,
|
||||
proposal: proposal
|
||||
)
|
||||
// Push a layout mutation if needed.
|
||||
if geometry != fiber.alternate?.geometry {
|
||||
caches.mutations.append(.layout(element: element, geometry: geometry))
|
||||
}
|
||||
// Update ours and our alternate's geometry
|
||||
fiber.geometry = geometry
|
||||
fiber.alternate?.geometry = geometry
|
||||
}
|
||||
|
||||
override func spacing() -> ViewSpacing {
|
||||
guard let fiber = fiber else { return .init() }
|
||||
|
||||
return caches.updateLayoutCache(for: fiber) { cache in
|
||||
fiber.layout?.spacing(
|
||||
subviews: caches.layoutSubviews(for: fiber),
|
||||
cache: &cache.cache
|
||||
) ?? .zero
|
||||
} ?? .zero
|
||||
}
|
||||
}
|
||||
|
||||
init<R: FiberRenderer>(
|
||||
id: ObjectIdentifier,
|
||||
traits: _ViewTraitStore?,
|
||||
fiber: FiberReconciler<R>.Fiber,
|
||||
element: R.ElementType,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) {
|
||||
self.id = id
|
||||
storage = Storage(
|
||||
traits: traits,
|
||||
fiber: fiber,
|
||||
element: element,
|
||||
caches: caches
|
||||
)
|
||||
}
|
||||
|
||||
public func _trait<K>(key: K.Type) -> K.Value where K: _ViewTraitKey {
|
||||
storage.traits?.value(forKey: key) ?? K.defaultValue
|
||||
}
|
||||
|
||||
public subscript<K>(key: K.Type) -> K.Value where K: LayoutValueKey {
|
||||
_trait(key: _LayoutTrait<K>.self)
|
||||
}
|
||||
|
||||
public var priority: Double {
|
||||
_trait(key: LayoutPriorityTraitKey.self)
|
||||
}
|
||||
|
||||
public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
||||
storage.sizeThatFits(proposal)
|
||||
}
|
||||
|
||||
public func dimensions(in proposal: ProposedViewSize) -> ViewDimensions {
|
||||
storage.dimensions(sizeThatFits(proposal))
|
||||
}
|
||||
|
||||
public var spacing: ViewSpacing {
|
||||
storage.spacing()
|
||||
}
|
||||
|
||||
public func place(
|
||||
at position: CGPoint,
|
||||
anchor: UnitPoint = .topLeading,
|
||||
proposal: ProposedViewSize
|
||||
) {
|
||||
storage.place(
|
||||
proposal,
|
||||
dimensions(in: proposal),
|
||||
position,
|
||||
anchor
|
||||
)
|
||||
}
|
||||
|
||||
public static func == (lhs: LayoutSubview, rhs: LayoutSubview) -> Bool {
|
||||
lhs.storage === rhs.storage
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
/// A key that stores a value that can be accessed via a `LayoutSubview`.
|
||||
public protocol LayoutValueKey {
|
||||
associatedtype Value
|
||||
static var defaultValue: Self.Value { get }
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func layoutValue<K>(key: K.Type, value: K.Value) -> some View where K: LayoutValueKey {
|
||||
// LayoutValueKey uses trait keys under the hood.
|
||||
_trait(_LayoutTrait<K>.self, value)
|
||||
}
|
||||
}
|
||||
|
||||
public struct _LayoutTrait<K>: _ViewTraitKey where K: LayoutValueKey {
|
||||
public static var defaultValue: K.Value {
|
||||
K.defaultValue
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private extension EdgeInsets {
|
||||
init(applying edges: Edge.Set, to insets: EdgeInsets) {
|
||||
self.init(
|
||||
top: edges.contains(.top) ? insets.top : 0,
|
||||
leading: edges.contains(.leading) ? insets.leading : 0,
|
||||
bottom: edges.contains(.bottom) ? insets.bottom : 0,
|
||||
trailing: edges.contains(.trailing) ? insets.trailing : 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PaddingLayout: Layout {
|
||||
let edges: Edge.Set
|
||||
let insets: EdgeInsets?
|
||||
|
||||
func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
|
||||
.init()
|
||||
}
|
||||
|
||||
public func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) -> CGSize {
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
let insets = EdgeInsets(applying: edges, to: insets ?? .init(_all: 10))
|
||||
let subviewSize = subviews.first?.sizeThatFits(
|
||||
.init(
|
||||
width: proposal.width - insets.leading - insets.trailing,
|
||||
height: proposal.height - insets.top - insets.bottom
|
||||
)
|
||||
) ?? .zero
|
||||
return .init(
|
||||
width: subviewSize.width + insets.leading + insets.trailing,
|
||||
height: subviewSize.height + insets.top + insets.bottom
|
||||
)
|
||||
}
|
||||
|
||||
public func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
let insets = EdgeInsets(applying: edges, to: insets ?? .init(_all: 10))
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
for subview in subviews {
|
||||
subview.place(
|
||||
at: .init(x: bounds.minX + insets.leading, y: bounds.minY + insets.top),
|
||||
proposal: .init(
|
||||
width: proposal.width - insets.leading - insets.trailing,
|
||||
height: proposal.height - insets.top - insets.bottom
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension _PaddingLayout {
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||
visitor.visit(PaddingLayout(edges: edges, insets: insets).callAsFunction {
|
||||
content
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct ProposedViewSize: Equatable {
|
||||
public var width: CGFloat?
|
||||
public var height: CGFloat?
|
||||
public static let zero: ProposedViewSize = .init(width: 0, height: 0)
|
||||
public static let unspecified: ProposedViewSize = .init(width: nil, height: nil)
|
||||
public static let infinity: ProposedViewSize = .init(width: .infinity, height: .infinity)
|
||||
@inlinable
|
||||
public init(width: CGFloat?, height: CGFloat?) {
|
||||
(self.width, self.height) = (width, height)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(_ size: CGSize) {
|
||||
self.init(width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func replacingUnspecifiedDimensions(by size: CGSize = CGSize(
|
||||
width: 10,
|
||||
height: 10
|
||||
)) -> CGSize {
|
||||
CGSize(width: width ?? size.width, height: height ?? size.height)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/24/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private extension ViewDimensions {
|
||||
/// Access the guide value of an `Alignment` for a particular `Axis`.
|
||||
subscript(alignment alignment: Alignment, in axis: Axis) -> CGFloat {
|
||||
switch axis {
|
||||
case .horizontal: return self[alignment.vertical]
|
||||
case .vertical: return self[alignment.horizontal]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Layout.Cache` for `StackLayout` conforming types.
|
||||
@_spi(TokamakCore)
|
||||
public struct StackLayoutCache {
|
||||
/// The widest/tallest (depending on the `axis`) subview.
|
||||
/// Used to place subviews along the `alignment`.
|
||||
var maxSubview: ViewDimensions?
|
||||
|
||||
/// The ideal size for each subview as computed in `sizeThatFits`.
|
||||
var idealSizes = [CGSize]()
|
||||
}
|
||||
|
||||
/// An internal structure used to store layout information about
|
||||
/// `LayoutSubview`s of a `StackLayout` that will later be sorted
|
||||
private struct MeasuredSubview {
|
||||
let view: LayoutSubview
|
||||
let index: Int
|
||||
let min: CGSize
|
||||
let max: CGSize
|
||||
let infiniteMainAxis: Bool
|
||||
let spacing: CGFloat
|
||||
}
|
||||
|
||||
/// The protocol all built-in stacks conform to.
|
||||
/// Provides a shared implementation for stack layout logic.
|
||||
@_spi(TokamakCore)
|
||||
public protocol StackLayout: Layout where Cache == StackLayoutCache {
|
||||
/// The direction of this stack. `vertical` for `VStack`, `horizontal` for `HStack`.
|
||||
static var orientation: Axis { get }
|
||||
|
||||
/// The full `Alignment` with an ignored value for the main axis.
|
||||
var _alignment: Alignment { get }
|
||||
|
||||
var spacing: CGFloat? { get }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public extension StackLayout {
|
||||
static var layoutProperties: LayoutProperties {
|
||||
var properties = LayoutProperties()
|
||||
properties.stackOrientation = Self.orientation
|
||||
return properties
|
||||
}
|
||||
|
||||
/// The `CGSize` component for the current `axis`.
|
||||
///
|
||||
/// A `vertical` axis will return `height`.
|
||||
/// A `horizontal` axis will return `width`.
|
||||
static var mainAxis: WritableKeyPath<CGSize, CGFloat> {
|
||||
switch Self.orientation {
|
||||
case .vertical: return \.height
|
||||
case .horizontal: return \.width
|
||||
}
|
||||
}
|
||||
|
||||
/// The `CGSize` component for the axis opposite `axis`.
|
||||
///
|
||||
/// A `vertical` axis will return `width`.
|
||||
/// A `horizontal` axis will return `height`.
|
||||
static var crossAxis: WritableKeyPath<CGSize, CGFloat> {
|
||||
switch Self.orientation {
|
||||
case .vertical: return \.width
|
||||
case .horizontal: return \.height
|
||||
}
|
||||
}
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
// Ensure we have enough space in `idealSizes` for each subview.
|
||||
.init(maxSubview: nil, idealSizes: Array(repeating: .zero, count: subviews.count))
|
||||
}
|
||||
|
||||
func updateCache(_ cache: inout Cache, subviews: Subviews) {
|
||||
cache.maxSubview = nil
|
||||
// Ensure we have enough space in `idealSizes` for each subview.
|
||||
cache.idealSizes = Array(repeating: .zero, count: subviews.count)
|
||||
}
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
|
||||
/// The minimum size of each `View` on the main axis.
|
||||
var minSize = CGFloat.zero
|
||||
|
||||
/// The aggregate `ViewSpacing` distances.
|
||||
var totalSpacing = CGFloat.zero
|
||||
|
||||
/// The number of `View`s with a given priority.
|
||||
var priorityCount = [Double: Int]()
|
||||
|
||||
/// The aggregate minimum size of each `View` with a given priority.
|
||||
var prioritySize = [Double: CGFloat]()
|
||||
let measuredSubviews = subviews.enumerated().map { index, view -> MeasuredSubview in
|
||||
priorityCount[view.priority, default: 0] += 1
|
||||
|
||||
var minProposal = CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
|
||||
minProposal[keyPath: Self.crossAxis] = proposal[keyPath: Self.crossAxis]
|
||||
minProposal[keyPath: Self.mainAxis] = 0
|
||||
/// The minimum size for this subview along the `mainAxis`.
|
||||
/// Uses `dimensions(in:)` to collect the alignment guides for use in `placeSubviews`.
|
||||
let min = view.dimensions(in: .init(minProposal))
|
||||
|
||||
// Aggregate the minimum size of the stack for the combined subviews.
|
||||
minSize += min.size[keyPath: Self.mainAxis]
|
||||
|
||||
// Aggregate the minimum size of this priority to divvy up space later.
|
||||
prioritySize[view.priority, default: 0] += min.size[keyPath: Self.mainAxis]
|
||||
|
||||
var maxProposal = CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
|
||||
maxProposal[keyPath: Self.crossAxis] = minProposal[keyPath: Self.crossAxis]
|
||||
/// The maximum size for this subview along the `mainAxis`.
|
||||
let max = view.sizeThatFits(.init(maxProposal))
|
||||
|
||||
/// The spacing around this `View` and its previous (if it is not first).
|
||||
let spacing: CGFloat
|
||||
if subviews.indices.contains(index - 1) {
|
||||
if let overrideSpacing = self.spacing {
|
||||
spacing = overrideSpacing
|
||||
} else {
|
||||
spacing = subviews[index - 1].spacing.distance(to: view.spacing, along: Self.orientation)
|
||||
}
|
||||
} else {
|
||||
spacing = .zero
|
||||
}
|
||||
// Aggregate all spacing values.
|
||||
totalSpacing += spacing
|
||||
|
||||
// If this `View` is the widest, save it to the cache for access in `placeSubviews`.
|
||||
if min.size[keyPath: Self.crossAxis] > cache.maxSubview?.size[keyPath: Self.crossAxis]
|
||||
?? .zero
|
||||
{
|
||||
cache.maxSubview = min
|
||||
}
|
||||
|
||||
return MeasuredSubview(
|
||||
view: view,
|
||||
index: index,
|
||||
min: min.size,
|
||||
max: max,
|
||||
infiniteMainAxis: max[keyPath: Self.mainAxis] == .infinity,
|
||||
spacing: spacing
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate ideal sizes for each View based on their min/max sizes and the space available.
|
||||
var available = proposal[keyPath: Self.mainAxis] - minSize - totalSpacing
|
||||
// The final resulting size.
|
||||
var size = CGSize.zero
|
||||
size[keyPath: Self.crossAxis] = cache.maxSubview?.size[keyPath: Self.crossAxis] ?? .zero
|
||||
for subview in measuredSubviews.sorted(by: {
|
||||
// Sort by priority descending.
|
||||
if $0.view.priority == $1.view.priority {
|
||||
// If the priorities match, allow non-flexible `View`s to size first.
|
||||
return $1.infiniteMainAxis && !$0.infiniteMainAxis
|
||||
} else {
|
||||
return $0.view.priority > $1.view.priority
|
||||
}
|
||||
}) {
|
||||
// The amount of space available to `View`s with this priority value.
|
||||
let priorityAvailable = available + prioritySize[subview.view.priority, default: 0]
|
||||
// The number of `View`s with this priority value remaining as a `CGFloat`.
|
||||
let priorityRemaining = CGFloat(priorityCount[subview.view.priority, default: 1])
|
||||
// Propose the full `crossAxis`, but only the remaining `mainAxis`.
|
||||
// Divvy up the available space between each remaining `View` with this priority value.
|
||||
var divviedSize = proposal
|
||||
divviedSize[keyPath: Self.mainAxis] = priorityAvailable / priorityRemaining
|
||||
let idealSize = subview.view.sizeThatFits(.init(divviedSize))
|
||||
cache.idealSizes[subview.index] = idealSize
|
||||
size[keyPath: Self.mainAxis] += idealSize[keyPath: Self.mainAxis] + subview.spacing
|
||||
// Remove our `idealSize` from the `available` space.
|
||||
available -= idealSize[keyPath: Self.mainAxis]
|
||||
// Decrement the number of `View`s left with this priority so space can be evenly divided
|
||||
// between the remaining `View`s.
|
||||
priorityCount[subview.view.priority, default: 1] -= 1
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing {
|
||||
subviews.reduce(into: .zero) { $0.formUnion($1.spacing) }
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
// The current progress along the `mainAxis`.
|
||||
var position = CGFloat.zero
|
||||
// The offset of the `_alignment` in the `maxSubview`,
|
||||
// used as the reference point for alignments along this axis.
|
||||
let alignmentOffset = cache.maxSubview?[alignment: _alignment, in: Self.orientation] ?? .zero
|
||||
for (index, view) in subviews.enumerated() {
|
||||
// Add a gap for the spacing distance from the previous subview to this one.
|
||||
let spacing: CGFloat
|
||||
if subviews.indices.contains(index - 1) {
|
||||
if let overrideSpacing = self.spacing {
|
||||
spacing = overrideSpacing
|
||||
} else {
|
||||
spacing = subviews[index - 1].spacing.distance(to: view.spacing, along: Self.orientation)
|
||||
}
|
||||
} else {
|
||||
spacing = .zero
|
||||
}
|
||||
position += spacing
|
||||
|
||||
let proposal = ProposedViewSize(cache.idealSizes[index])
|
||||
let size = view.dimensions(in: proposal)
|
||||
|
||||
// Offset the placement along the `crossAxis` to align with the
|
||||
// `alignment` of the `maxSubview`.
|
||||
var placement = CGSize(width: bounds.minX, height: bounds.minY)
|
||||
placement[keyPath: Self.mainAxis] += position
|
||||
placement[keyPath: Self.crossAxis] += alignmentOffset
|
||||
- size[alignment: _alignment, in: Self.orientation]
|
||||
|
||||
view.place(
|
||||
at: .init(
|
||||
x: placement.width,
|
||||
y: placement.height
|
||||
),
|
||||
proposal: proposal
|
||||
)
|
||||
// Move further along the stack's `mainAxis`.
|
||||
position += size.size[keyPath: Self.mainAxis]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension VStack: StackLayout {
|
||||
public static var orientation: Axis { .vertical }
|
||||
public var _alignment: Alignment { .init(horizontal: alignment, vertical: .center) }
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
extension HStack: StackLayout {
|
||||
public static var orientation: Axis { .horizontal }
|
||||
public var _alignment: Alignment { .init(horizontal: .center, vertical: alignment) }
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The preferred spacing around a `View`.
|
||||
///
|
||||
/// When computing spacing in a custom `Layout`, use `distance(to:along:)`
|
||||
/// to find the smallest spacing needed to accommodate the preferences
|
||||
/// of the `View`s you are aligning.
|
||||
public struct ViewSpacing {
|
||||
/// The `View` type this `ViewSpacing` is for.
|
||||
/// Some `View`s prefer different spacing based on the `View` they are adjacent to.
|
||||
@_spi(TokamakCore)
|
||||
public var viewType: Any.Type?
|
||||
|
||||
private var top: (ViewSpacing) -> CGFloat
|
||||
private var leading: (ViewSpacing) -> CGFloat
|
||||
private var bottom: (ViewSpacing) -> CGFloat
|
||||
private var trailing: (ViewSpacing) -> CGFloat
|
||||
|
||||
public static let zero: ViewSpacing = .init(
|
||||
viewType: nil,
|
||||
top: { _ in 0 },
|
||||
leading: { _ in 0 },
|
||||
bottom: { _ in 0 },
|
||||
trailing: { _ in 0 }
|
||||
)
|
||||
|
||||
/// Create a `ViewSpacing` instance with default values.
|
||||
public init() {
|
||||
self.init(viewType: nil)
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public static let defaultValue: CGFloat = 8
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public init(
|
||||
viewType: Any.Type?,
|
||||
top: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
|
||||
leading: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
|
||||
bottom: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue },
|
||||
trailing: @escaping (ViewSpacing) -> CGFloat = { _ in Self.defaultValue }
|
||||
) {
|
||||
self.viewType = viewType
|
||||
self.top = top
|
||||
self.leading = leading
|
||||
self.bottom = bottom
|
||||
self.trailing = trailing
|
||||
}
|
||||
|
||||
public mutating func formUnion(_ other: ViewSpacing, edges: Edge.Set = .all) {
|
||||
if viewType != other.viewType {
|
||||
viewType = nil
|
||||
}
|
||||
if edges.contains(.top) {
|
||||
let current = top
|
||||
top = { max(current($0), other.top($0)) }
|
||||
}
|
||||
if edges.contains(.leading) {
|
||||
let current = leading
|
||||
leading = { max(current($0), other.leading($0)) }
|
||||
}
|
||||
if edges.contains(.bottom) {
|
||||
let current = bottom
|
||||
bottom = { max(current($0), other.bottom($0)) }
|
||||
}
|
||||
if edges.contains(.trailing) {
|
||||
let current = trailing
|
||||
trailing = { max(current($0), other.trailing($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
public func union(_ other: ViewSpacing, edges: Edge.Set = .all) -> ViewSpacing {
|
||||
var spacing = self
|
||||
spacing.formUnion(other, edges: edges)
|
||||
return spacing
|
||||
}
|
||||
|
||||
/// The smallest spacing that accommodates the preferences of `self` and `next`.
|
||||
public func distance(to next: ViewSpacing, along axis: Axis) -> CGFloat {
|
||||
// Assume `next` comes after `self` either horizontally or vertically.
|
||||
switch axis {
|
||||
case .horizontal:
|
||||
return max(trailing(next), next.leading(self))
|
||||
case .vertical:
|
||||
return max(bottom(next), next.top(self))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private struct AspectRatioLayout: Layout {
|
||||
let aspectRatio: CGFloat?
|
||||
let contentMode: ContentMode
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
let proposal = proposal.replacingUnspecifiedDimensions()
|
||||
let aspectRatio: CGFloat
|
||||
if let ratio = self.aspectRatio {
|
||||
aspectRatio = ratio
|
||||
} else {
|
||||
let idealSubviewSize = subviews.first?.sizeThatFits(.unspecified) ?? .zero
|
||||
if idealSubviewSize.height == 0 {
|
||||
aspectRatio = 0
|
||||
} else {
|
||||
aspectRatio = idealSubviewSize.width / idealSubviewSize.height
|
||||
}
|
||||
}
|
||||
let maxAxis: Axis
|
||||
switch contentMode {
|
||||
case .fit:
|
||||
if proposal.width == proposal.height {
|
||||
if aspectRatio >= 1 {
|
||||
maxAxis = .vertical
|
||||
} else {
|
||||
maxAxis = .horizontal
|
||||
}
|
||||
} else if proposal.width > proposal.height {
|
||||
maxAxis = .horizontal
|
||||
} else {
|
||||
maxAxis = .vertical
|
||||
}
|
||||
case .fill:
|
||||
if proposal.width == proposal.height {
|
||||
if aspectRatio >= 1 {
|
||||
maxAxis = .horizontal
|
||||
} else {
|
||||
maxAxis = .vertical
|
||||
}
|
||||
} else if proposal.width > proposal.height {
|
||||
maxAxis = .vertical
|
||||
} else {
|
||||
maxAxis = .horizontal
|
||||
}
|
||||
}
|
||||
switch maxAxis {
|
||||
case .horizontal:
|
||||
return .init(
|
||||
width: aspectRatio * proposal.height,
|
||||
height: proposal.height
|
||||
)
|
||||
case .vertical:
|
||||
return .init(
|
||||
width: proposal.width,
|
||||
height: (1 / aspectRatio) * proposal.width
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
for subview in subviews {
|
||||
subview.place(
|
||||
at: .init(x: bounds.midX, y: bounds.midY),
|
||||
anchor: .center,
|
||||
proposal: .init(bounds.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension _AspectRatioLayout {
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||
visitor.visit(
|
||||
AspectRatioLayout(
|
||||
aspectRatio: aspectRatio,
|
||||
contentMode: contentMode
|
||||
)
|
||||
.callAsFunction {
|
||||
content
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A `Layout` container that creates a frame with constraints.
|
||||
///
|
||||
/// The children are proposed the full proposal given to this container
|
||||
/// clamped to the specified minimum and maximum values.
|
||||
///
|
||||
/// Then the children are placed with `alignment` in the container.
|
||||
private struct FlexFrameLayout: Layout {
|
||||
let minWidth: CGFloat?
|
||||
let idealWidth: CGFloat?
|
||||
let maxWidth: CGFloat?
|
||||
let minHeight: CGFloat?
|
||||
let idealHeight: CGFloat?
|
||||
let maxHeight: CGFloat?
|
||||
let alignment: Alignment
|
||||
|
||||
struct Cache {
|
||||
var dimensions = [ViewDimensions]()
|
||||
}
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
.init()
|
||||
}
|
||||
|
||||
func updateCache(_ cache: inout Cache, subviews: Subviews) {
|
||||
cache.dimensions.removeAll()
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
let bounds = CGSize(
|
||||
width: min(
|
||||
max(minWidth ?? .zero, proposal.width ?? idealWidth ?? .zero),
|
||||
maxWidth ?? CGFloat.infinity
|
||||
),
|
||||
height: min(
|
||||
max(minHeight ?? .zero, proposal.height ?? idealHeight ?? .zero),
|
||||
maxHeight ?? CGFloat.infinity
|
||||
)
|
||||
)
|
||||
let proposal = ProposedViewSize(bounds)
|
||||
|
||||
var subviewSizes = CGSize.zero
|
||||
cache.dimensions = subviews.map { subview -> ViewDimensions in
|
||||
let dimensions = subview.dimensions(in: proposal)
|
||||
if dimensions.width > subviewSizes.width {
|
||||
subviewSizes.width = dimensions.width
|
||||
}
|
||||
if dimensions.height > subviewSizes.height {
|
||||
subviewSizes.height = dimensions.height
|
||||
}
|
||||
return dimensions
|
||||
}
|
||||
|
||||
var size = CGSize.zero
|
||||
if let minWidth = minWidth,
|
||||
bounds.width < subviewSizes.width
|
||||
{
|
||||
size.width = max(bounds.width, minWidth)
|
||||
} else if let maxWidth = maxWidth,
|
||||
bounds.width > subviewSizes.width
|
||||
{
|
||||
size.width = min(bounds.width, maxWidth)
|
||||
} else {
|
||||
size.width = subviewSizes.width
|
||||
}
|
||||
if let minHeight = minHeight,
|
||||
bounds.height < subviewSizes.height
|
||||
{
|
||||
size.height = max(bounds.height, minHeight)
|
||||
} else if let maxHeight = maxHeight,
|
||||
bounds.height > subviewSizes.height
|
||||
{
|
||||
size.height = min(bounds.height, maxHeight)
|
||||
} else {
|
||||
size.height = subviewSizes.height
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
let proposal = ProposedViewSize(bounds.size)
|
||||
let frameDimensions = ViewDimensions(
|
||||
size: .init(width: bounds.width, height: bounds.height),
|
||||
alignmentGuides: [:]
|
||||
)
|
||||
|
||||
for (index, subview) in subviews.enumerated() {
|
||||
subview.place(
|
||||
at: .init(
|
||||
x: bounds.minX + frameDimensions[alignment.horizontal]
|
||||
- cache.dimensions[index][alignment.horizontal],
|
||||
y: bounds.minY + frameDimensions[alignment.vertical]
|
||||
- cache.dimensions[index][alignment.vertical]
|
||||
),
|
||||
proposal: proposal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension _FlexFrameLayout {
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||
visitor.visit(FlexFrameLayout(
|
||||
minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth,
|
||||
minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight,
|
||||
alignment: alignment
|
||||
).callAsFunction {
|
||||
content
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A `Layout` container that requests a specific size on one or more axes.
|
||||
///
|
||||
/// The container proposes the constrained size to its children,
|
||||
/// then places them with `alignment` in the constrained bounds.
|
||||
///
|
||||
/// Children request their own size, so they may overflow this container.
|
||||
///
|
||||
/// If no fixed size is specified for a an axis, the container will use the size of its children.
|
||||
private struct FrameLayout: Layout {
|
||||
let width: CGFloat?
|
||||
let height: CGFloat?
|
||||
let alignment: Alignment
|
||||
|
||||
struct Cache {
|
||||
var dimensions = [ViewDimensions]()
|
||||
}
|
||||
|
||||
func makeCache(subviews: Subviews) -> Cache {
|
||||
.init()
|
||||
}
|
||||
|
||||
func updateCache(_ cache: inout Cache, subviews: Subviews) {
|
||||
cache.dimensions.removeAll()
|
||||
}
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) -> CGSize {
|
||||
var size = CGSize.zero
|
||||
let proposal = ProposedViewSize(
|
||||
width: width ?? proposal.width,
|
||||
height: height ?? proposal.height
|
||||
)
|
||||
cache.dimensions = subviews.map { subview -> ViewDimensions in
|
||||
let dimensions = subview.dimensions(in: proposal)
|
||||
if dimensions.width > size.width {
|
||||
size.width = dimensions.width
|
||||
}
|
||||
if dimensions.height > size.height {
|
||||
size.height = dimensions.height
|
||||
}
|
||||
return dimensions
|
||||
}
|
||||
return .init(
|
||||
width: width ?? size.width,
|
||||
height: height ?? size.height
|
||||
)
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout Cache
|
||||
) {
|
||||
let proposal = ProposedViewSize(bounds.size)
|
||||
let frameDimensions = ViewDimensions(
|
||||
size: .init(width: bounds.width, height: bounds.height),
|
||||
alignmentGuides: [:]
|
||||
)
|
||||
|
||||
for (index, subview) in subviews.enumerated() {
|
||||
subview.place(
|
||||
at: .init(
|
||||
x: bounds.minX + frameDimensions[alignment.horizontal]
|
||||
- cache.dimensions[index][alignment.horizontal],
|
||||
y: bounds.minY + frameDimensions[alignment.vertical]
|
||||
- cache.dimensions[index][alignment.vertical]
|
||||
),
|
||||
proposal: proposal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension _FrameLayout {
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||
visitor.visit(FrameLayout(width: width, height: height, alignment: alignment).callAsFunction {
|
||||
content
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Mutation<Renderer: FiberRenderer> {
|
||||
case insert(
|
||||
element: Renderer.ElementType,
|
||||
parent: Renderer.ElementType,
|
||||
index: Int
|
||||
)
|
||||
case remove(element: Renderer.ElementType, parent: Renderer.ElementType?)
|
||||
case replace(
|
||||
parent: Renderer.ElementType,
|
||||
previous: Renderer.ElementType,
|
||||
replacement: Renderer.ElementType
|
||||
)
|
||||
case update(
|
||||
previous: Renderer.ElementType,
|
||||
newContent: Renderer.ElementType.Content,
|
||||
geometry: ViewGeometry
|
||||
)
|
||||
case layout(element: Renderer.ElementType, geometry: ViewGeometry)
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FiberReconciler {
|
||||
final class Caches {
|
||||
var elementIndices = [ObjectIdentifier: Int]()
|
||||
var layoutCaches = [ObjectIdentifier: LayoutCache]()
|
||||
var layoutSubviews = [ObjectIdentifier: LayoutSubviews]()
|
||||
var mutations = [Mutation<Renderer>]()
|
||||
|
||||
struct LayoutCache {
|
||||
/// The erased `Layout.Cache` value.
|
||||
var cache: AnyLayout.Cache
|
||||
|
||||
/// Cached values for `sizeThatFits` calls.
|
||||
var sizeThatFits: [SizeThatFitsRequest: CGSize]
|
||||
|
||||
/// Cached values for `dimensions(in:)` calls.
|
||||
var dimensions: [SizeThatFitsRequest: ViewDimensions]
|
||||
|
||||
/// Does this cache need to be updated before using?
|
||||
/// Set to `true` whenever the subviews or the container changes.
|
||||
var isDirty: Bool
|
||||
|
||||
/// Empty the cached values and flag the cache as dirty.
|
||||
mutating func markDirty() {
|
||||
isDirty = true
|
||||
sizeThatFits.removeAll()
|
||||
dimensions.removeAll()
|
||||
}
|
||||
|
||||
struct SizeThatFitsRequest: Hashable {
|
||||
let proposal: ProposedViewSize
|
||||
|
||||
@inlinable
|
||||
init(_ proposal: ProposedViewSize) {
|
||||
self.proposal = proposal
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(proposal.width)
|
||||
hasher.combine(proposal.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clear() {
|
||||
elementIndices.removeAll()
|
||||
layoutSubviews.removeAll()
|
||||
mutations.removeAll()
|
||||
}
|
||||
|
||||
func layoutCache(for fiber: Fiber) -> LayoutCache? {
|
||||
guard let layout = fiber.layout else { return nil }
|
||||
return layoutCaches[
|
||||
ObjectIdentifier(fiber),
|
||||
default: .init(
|
||||
cache: layout.makeCache(subviews: layoutSubviews(for: fiber)),
|
||||
sizeThatFits: [:],
|
||||
dimensions: [:],
|
||||
isDirty: false
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
func updateLayoutCache<R>(for fiber: Fiber, _ action: (inout LayoutCache) -> R) -> R? {
|
||||
guard let layout = fiber.layout else { return nil }
|
||||
let subviews = layoutSubviews(for: fiber)
|
||||
let key = ObjectIdentifier(fiber)
|
||||
var cache = layoutCaches[
|
||||
key,
|
||||
default: .init(
|
||||
cache: layout.makeCache(subviews: subviews),
|
||||
sizeThatFits: [:],
|
||||
dimensions: [:],
|
||||
isDirty: false
|
||||
)
|
||||
]
|
||||
// If the cache is dirty, update it before calling `action`.
|
||||
if cache.isDirty {
|
||||
layout.updateCache(&cache.cache, subviews: subviews)
|
||||
cache.isDirty = false
|
||||
}
|
||||
defer { layoutCaches[key] = cache }
|
||||
return action(&cache)
|
||||
}
|
||||
|
||||
func layoutSubviews(for fiber: Fiber) -> LayoutSubviews {
|
||||
layoutSubviews[ObjectIdentifier(fiber), default: .init(fiber)]
|
||||
}
|
||||
|
||||
func elementIndex(for fiber: Fiber, increment: Bool = false) -> Int {
|
||||
let key = ObjectIdentifier(fiber)
|
||||
let result = elementIndices[key, default: 0]
|
||||
if increment {
|
||||
elementIndices[key] = result + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol FiberReconcilerPass {
|
||||
/// Run this pass with the given inputs.
|
||||
///
|
||||
/// - Parameter reconciler: The `FiberReconciler` running this pass.
|
||||
/// - Parameter root: The node to start the pass from.
|
||||
/// The top of the `View` hierarchy when `useDynamicLayout` is enabled.
|
||||
/// Otherwise, the same as `reconcileRoot`.
|
||||
/// - Parameter reconcileRoot: A list of topmost nodes that need reconciliation.
|
||||
/// When `useDynamicLayout` is enabled, this can be used to limit
|
||||
/// the number of operations performed during reconciliation.
|
||||
/// - Parameter caches: The shared cache data for this and other passes.
|
||||
func run<R: FiberRenderer>(
|
||||
in reconciler: FiberReconciler<R>,
|
||||
root: FiberReconciler<R>.TreeReducer.Result,
|
||||
changedFibers: Set<ObjectIdentifier>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Layout from the top down.
|
||||
struct LayoutPass: FiberReconcilerPass {
|
||||
func run<R>(
|
||||
in reconciler: FiberReconciler<R>,
|
||||
root: FiberReconciler<R>.TreeReducer.Result,
|
||||
changedFibers: Set<ObjectIdentifier>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) where R: FiberRenderer {
|
||||
guard let root = root.fiber else { return }
|
||||
var fiber = root
|
||||
|
||||
while true {
|
||||
// Place subviews for each element fiber as we walk the tree.
|
||||
if fiber.element != nil {
|
||||
caches.updateLayoutCache(for: fiber) { cache in
|
||||
fiber.layout?.placeSubviews(
|
||||
in: .init(
|
||||
origin: .zero,
|
||||
size: fiber.geometry?.dimensions.size ?? reconciler.renderer.sceneSize.value
|
||||
),
|
||||
proposal: fiber.geometry?.proposal ?? .unspecified,
|
||||
subviews: caches.layoutSubviews(for: fiber),
|
||||
cache: &cache.cache
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if let child = fiber.child {
|
||||
// Continue down the tree.
|
||||
fiber = child
|
||||
continue
|
||||
}
|
||||
|
||||
while fiber.sibling == nil {
|
||||
// Exit at the top of the `View` tree
|
||||
guard let parent = fiber.parent else { return }
|
||||
guard parent !== root else { return }
|
||||
// Walk up to the next parent.
|
||||
fiber = parent
|
||||
}
|
||||
|
||||
// Walk across to the next sibling.
|
||||
fiber = fiber.sibling!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FiberReconcilerPass where Self == LayoutPass {
|
||||
static var layout: LayoutPass { .init() }
|
||||
}
|
|
@ -0,0 +1,296 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 6/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Walk the current tree, recomputing at each step to check for discrepancies.
|
||||
///
|
||||
/// Parent-first depth-first traversal.
|
||||
/// Take this `View` tree for example.
|
||||
/// ```swift
|
||||
/// VStack {
|
||||
/// HStack {
|
||||
/// Text("A")
|
||||
/// Text("B")
|
||||
/// }
|
||||
/// Text("C")
|
||||
/// }
|
||||
/// ```
|
||||
/// Basically, we read it like this:
|
||||
/// 1. `VStack` has children, so we go to it's first child, `HStack`.
|
||||
/// 2. `HStack` has children, so we go further to it's first child, `Text`.
|
||||
/// 3. `Text` has no child, but has a sibling, so we go to that.
|
||||
/// 4. `Text` has no child and no sibling, so we return to the `HStack`.
|
||||
/// 5. We've already read the children, so we look for a sibling, `Text`.
|
||||
/// 6. `Text` has no children and no sibling, so we return to the `VStack.`
|
||||
/// We finish once we've returned to the root element.
|
||||
/// ```
|
||||
/// ┌──────┐
|
||||
/// │VStack│
|
||||
/// └──┬───┘
|
||||
/// ▲ 1 │
|
||||
/// │ └──►┌──────┐
|
||||
/// │ │HStack│
|
||||
/// │ ┌─┴───┬──┘
|
||||
/// │ │ ▲ │ 2
|
||||
/// │ │ │ │ ┌────┐
|
||||
/// │ │ │ └─►│Text├─┐
|
||||
/// 6 │ │ 4 │ └────┘ │
|
||||
/// │ │ │ │ 3
|
||||
/// │ 5 │ │ ┌────┐ │
|
||||
/// │ │ └────┤Text│◄┘
|
||||
/// │ │ └────┘
|
||||
/// │ │
|
||||
/// │ └►┌────┐
|
||||
/// │ │Text│
|
||||
/// └───────┴────┘
|
||||
/// ```
|
||||
struct ReconcilePass: FiberReconcilerPass {
|
||||
func run<R>(
|
||||
in reconciler: FiberReconciler<R>,
|
||||
root: FiberReconciler<R>.TreeReducer.Result,
|
||||
changedFibers: Set<ObjectIdentifier>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) where R: FiberRenderer {
|
||||
var node = root
|
||||
|
||||
// Enabled when we reach the `reconcileRoot`.
|
||||
var shouldReconcile = false
|
||||
|
||||
while true {
|
||||
if !shouldReconcile {
|
||||
if let fiber = node.fiber,
|
||||
changedFibers.contains(ObjectIdentifier(fiber))
|
||||
{
|
||||
shouldReconcile = true
|
||||
} else if let alternate = node.fiber?.alternate,
|
||||
changedFibers.contains(ObjectIdentifier(alternate))
|
||||
{
|
||||
shouldReconcile = true
|
||||
}
|
||||
}
|
||||
|
||||
// If this fiber has an element, set its `elementIndex`
|
||||
// and increment the `elementIndices` value for its `elementParent`.
|
||||
if node.fiber?.element != nil,
|
||||
let elementParent = node.fiber?.elementParent
|
||||
{
|
||||
node.fiber?.elementIndex = caches.elementIndex(for: elementParent, increment: true)
|
||||
}
|
||||
|
||||
// Perform work on the node.
|
||||
if shouldReconcile,
|
||||
let mutation = reconcile(node, in: reconciler, caches: caches)
|
||||
{
|
||||
caches.mutations.append(mutation)
|
||||
}
|
||||
|
||||
// Ensure the `TreeReducer` can access any necessary state.
|
||||
node.elementIndices = caches.elementIndices
|
||||
// Pass view traits down to the nearest element fiber.
|
||||
if let traits = node.fiber?.outputs.traits,
|
||||
!traits.values.isEmpty
|
||||
{
|
||||
node.nextTraits.values.merge(traits.values, uniquingKeysWith: { $1 })
|
||||
}
|
||||
|
||||
// Update `DynamicProperty`s before accessing the `View`'s body.
|
||||
node.fiber?.updateDynamicProperties()
|
||||
// Compute the children of the node.
|
||||
let reducer = FiberReconciler<R>.TreeReducer.SceneVisitor(initialResult: node)
|
||||
node.visitChildren(reducer)
|
||||
|
||||
node.fiber?.preferences?.reset()
|
||||
|
||||
if reconciler.renderer.useDynamicLayout,
|
||||
let fiber = node.fiber
|
||||
{
|
||||
if let element = fiber.element,
|
||||
let elementParent = fiber.elementParent
|
||||
{
|
||||
let parentKey = ObjectIdentifier(elementParent)
|
||||
let subview = LayoutSubview(
|
||||
id: ObjectIdentifier(fiber),
|
||||
traits: fiber.outputs.traits,
|
||||
fiber: fiber,
|
||||
element: element,
|
||||
caches: caches
|
||||
)
|
||||
caches.layoutSubviews[parentKey, default: .init(elementParent)].storage.append(subview)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the alternate if it doesn't exist yet.
|
||||
if node.fiber?.alternate == nil {
|
||||
_ = node.fiber?.createAndBindAlternate?()
|
||||
}
|
||||
|
||||
// Walk down all the way into the deepest child.
|
||||
if let child = reducer.result.child {
|
||||
node = child
|
||||
continue
|
||||
} else if let alternateChild = node.fiber?.alternate?.child {
|
||||
// The alternate has a child that no longer exists.
|
||||
if let parent = node.fiber?.element != nil ? node.fiber : node.fiber?.elementParent {
|
||||
invalidateCache(for: parent, in: reconciler, caches: caches)
|
||||
}
|
||||
walk(alternateChild) { node in
|
||||
if let element = node.element,
|
||||
let parent = node.elementParent?.element
|
||||
{
|
||||
// Removals must happen in reverse order, so a child element
|
||||
// is removed before its parent.
|
||||
caches.mutations.insert(.remove(element: element, parent: parent), at: 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if reducer.result.child == nil {
|
||||
// Make sure we clear the child if there was none
|
||||
node.fiber?.child = nil
|
||||
node.fiber?.alternate?.child = nil
|
||||
}
|
||||
|
||||
// If we've made it back to the root, then exit.
|
||||
if node === root {
|
||||
return
|
||||
}
|
||||
|
||||
// Now walk back up the tree until we find a sibling.
|
||||
while node.sibling == nil {
|
||||
if let fiber = node.fiber,
|
||||
fiber.element != nil
|
||||
{
|
||||
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
|
||||
if let preferences = node.fiber?.preferences {
|
||||
if let action = node.fiber?.outputs.preferenceAction {
|
||||
action(preferences)
|
||||
}
|
||||
if let parentPreferences = node.fiber?.preferenceParent?.preferences {
|
||||
parentPreferences.merge(preferences)
|
||||
}
|
||||
}
|
||||
|
||||
var alternateSibling = node.fiber?.alternate?.sibling
|
||||
// The alternate had siblings that no longer exist.
|
||||
while alternateSibling != nil {
|
||||
if let fiber = alternateSibling?.elementParent {
|
||||
invalidateCache(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
if let element = alternateSibling?.element,
|
||||
let parent = alternateSibling?.elementParent?.element
|
||||
{
|
||||
// Removals happen in reverse order, so a child element is removed before
|
||||
// its parent.
|
||||
caches.mutations.insert(.remove(element: element, parent: parent), at: 0)
|
||||
}
|
||||
alternateSibling = alternateSibling?.sibling
|
||||
}
|
||||
guard let parent = node.parent else { return }
|
||||
// When we walk back to the root, exit
|
||||
guard parent !== root.fiber?.alternate else { return }
|
||||
node = parent
|
||||
}
|
||||
|
||||
if let fiber = node.fiber {
|
||||
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
|
||||
// Walk across to the sibling, and repeat.
|
||||
node = node.sibling!
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare `node` with its alternate, and add any mutations to the list.
|
||||
func reconcile<R: FiberRenderer>(
|
||||
_ node: FiberReconciler<R>.TreeReducer.Result,
|
||||
in reconciler: FiberReconciler<R>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) -> Mutation<R>? {
|
||||
if let element = node.fiber?.element,
|
||||
let index = node.fiber?.elementIndex,
|
||||
let parent = node.fiber?.elementParent?.element
|
||||
{
|
||||
if node.fiber?.alternate == nil { // This didn't exist before (no alternate)
|
||||
if let fiber = node.fiber {
|
||||
invalidateCache(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
return .insert(element: element, parent: parent, index: index)
|
||||
} else if node.fiber?.typeInfo?.type != node.fiber?.alternate?.typeInfo?.type,
|
||||
let previous = node.fiber?.alternate?.element
|
||||
{
|
||||
if let fiber = node.fiber {
|
||||
invalidateCache(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
// This is a completely different type of view.
|
||||
return .replace(parent: parent, previous: previous, replacement: element)
|
||||
} else if let newContent = node.newContent,
|
||||
newContent != element.content
|
||||
{
|
||||
if let fiber = node.fiber {
|
||||
invalidateCache(for: fiber, in: reconciler, caches: caches)
|
||||
}
|
||||
// This is the same type of view, but its backing data has changed.
|
||||
return .update(
|
||||
previous: element,
|
||||
newContent: newContent,
|
||||
geometry: node.fiber?.geometry ?? .init(
|
||||
origin: .init(origin: .zero),
|
||||
dimensions: .init(size: .zero, alignmentGuides: [:]),
|
||||
proposal: .unspecified
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Remove cached size values if something changed.
|
||||
func invalidateCache<R: FiberRenderer>(
|
||||
for fiber: FiberReconciler<R>.Fiber,
|
||||
in reconciler: FiberReconciler<R>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) {
|
||||
guard reconciler.renderer.useDynamicLayout else { return }
|
||||
caches.updateLayoutCache(for: fiber) { cache in
|
||||
cache.markDirty()
|
||||
}
|
||||
if let alternate = fiber.alternate {
|
||||
caches.updateLayoutCache(for: alternate) { cache in
|
||||
cache.markDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func propagateCacheInvalidation<R: FiberRenderer>(
|
||||
for fiber: FiberReconciler<R>.Fiber,
|
||||
in reconciler: FiberReconciler<R>,
|
||||
caches: FiberReconciler<R>.Caches
|
||||
) {
|
||||
guard caches.layoutCache(for: fiber)?.isDirty ?? false,
|
||||
let elementParent = fiber.elementParent
|
||||
else { return }
|
||||
invalidateCache(for: elementParent, in: reconciler, caches: caches)
|
||||
}
|
||||
}
|
||||
|
||||
extension FiberReconcilerPass where Self == ReconcilePass {
|
||||
static var reconcile: ReconcilePass { ReconcilePass() }
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/30/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Scene {
|
||||
// By default, we simply pass the inputs through without modifications.
|
||||
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs {
|
||||
.init(inputs: inputs)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 5/30/22.
|
||||
//
|
||||
|
||||
/// A type that can visit a `Scene`.
|
||||
public protocol SceneVisitor: ViewVisitor {
|
||||
func visit<S: Scene>(_ scene: S)
|
||||
}
|
||||
|
||||
public extension Scene {
|
||||
func _visitChildren<V: SceneVisitor>(_ visitor: V) {
|
||||
visitor.visit(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that creates a `Result` by visiting multiple `Scene`s.
|
||||
protocol SceneReducer: ViewReducer {
|
||||
associatedtype Result
|
||||
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S)
|
||||
static func reduce<S: Scene>(partialResult: Result, nextScene: S) -> Result
|
||||
}
|
||||
|
||||
extension SceneReducer {
|
||||
static func reduce<S: Scene>(into partialResult: inout Result, nextScene: S) {
|
||||
partialResult = Self.reduce(partialResult: partialResult, nextScene: nextScene)
|
||||
}
|
||||
|
||||
static func reduce<S: Scene>(partialResult: Result, nextScene: S) -> Result {
|
||||
var result = partialResult
|
||||
Self.reduce(into: &result, nextScene: nextScene)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// A `SceneVisitor` that uses a `SceneReducer`
|
||||
/// to collapse the `Scene` values into a single `Result`.
|
||||
final class SceneReducerVisitor<R: SceneReducer>: SceneVisitor {
|
||||
var result: R.Result
|
||||
|
||||
init(initialResult: R.Result) {
|
||||
result = initialResult
|
||||
}
|
||||
|
||||
func visit<S>(_ scene: S) where S: Scene {
|
||||
R.reduce(into: &result, nextScene: scene)
|
||||
}
|
||||
|
||||
func visit<V>(_ view: V) where V: View {
|
||||
R.reduce(into: &result, nextView: view)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneReducer {
|
||||
typealias SceneVisitor = SceneReducerVisitor<Self>
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/7/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Data passed to `_makeView` to create the `ViewOutputs` used in reconciling/rendering.
|
||||
public struct ViewInputs<V> {
|
||||
public let content: V
|
||||
|
||||
/// Mutate the underlying content with the given inputs.
|
||||
///
|
||||
/// Used to inject values such as environment values, traits, and preferences into the `View` type.
|
||||
public let updateContent: ((inout V) -> ()) -> ()
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public let environment: EnvironmentBox
|
||||
|
||||
public let traits: _ViewTraitStore?
|
||||
|
||||
public let preferenceStore: _PreferenceStore?
|
||||
}
|
||||
|
||||
/// Data used to reconcile and render a `View` and its children.
|
||||
public struct ViewOutputs {
|
||||
/// A container for the current `EnvironmentValues`.
|
||||
/// This is stored as a reference to avoid copying the environment when unnecessary.
|
||||
let environment: EnvironmentBox
|
||||
|
||||
let preferenceStore: _PreferenceStore?
|
||||
|
||||
/// An action to perform after all preferences values have been reduced.
|
||||
///
|
||||
/// Called when walking back up the tree in the `ReconcilePass`.
|
||||
let preferenceAction: ((_PreferenceStore) -> ())?
|
||||
|
||||
let traits: _ViewTraitStore?
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public final class EnvironmentBox {
|
||||
public let environment: EnvironmentValues
|
||||
|
||||
public init(_ environment: EnvironmentValues) {
|
||||
self.environment = environment
|
||||
}
|
||||
}
|
||||
|
||||
public extension ViewOutputs {
|
||||
init<V>(
|
||||
inputs: ViewInputs<V>,
|
||||
environment: EnvironmentValues? = nil,
|
||||
preferenceStore: _PreferenceStore? = nil,
|
||||
preferenceAction: ((_PreferenceStore) -> ())? = nil,
|
||||
traits: _ViewTraitStore? = nil
|
||||
) {
|
||||
// Only replace the `EnvironmentBox` when we change the environment.
|
||||
// Otherwise the same box can be reused.
|
||||
self.environment = environment.map(EnvironmentBox.init) ?? inputs.environment
|
||||
self.preferenceStore = preferenceStore
|
||||
self.preferenceAction = preferenceAction
|
||||
self.traits = traits ?? inputs.traits
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
// By default, we simply pass the inputs through without modifications
|
||||
// or layout considerations.
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
.init(inputs: inputs)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ModifiedContent where Content: View, Modifier: ViewModifier {
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
Modifier._makeView(.init(
|
||||
content: inputs.content.modifier,
|
||||
updateContent: { _ in },
|
||||
environment: inputs.environment,
|
||||
traits: inputs.traits,
|
||||
preferenceStore: inputs.preferenceStore
|
||||
))
|
||||
}
|
||||
|
||||
func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
modifier._visitChildren(visitor, content: .init(modifier: modifier, view: content))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/17/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ViewGeometry: Equatable {
|
||||
@_spi(TokamakCore)
|
||||
public let origin: ViewOrigin
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public let dimensions: ViewDimensions
|
||||
|
||||
let proposal: ProposedViewSize
|
||||
}
|
||||
|
||||
/// The position of the `View` relative to its parent.
|
||||
public struct ViewOrigin: Equatable {
|
||||
@_spi(TokamakCore)
|
||||
public let origin: CGPoint
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public var x: CGFloat { origin.x }
|
||||
@_spi(TokamakCore)
|
||||
public var y: CGFloat { origin.y }
|
||||
}
|
||||
|
||||
public struct ViewDimensions: Equatable {
|
||||
@_spi(TokamakCore)
|
||||
public let size: CGSize
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public let alignmentGuides: [ObjectIdentifier: CGFloat]
|
||||
|
||||
public var width: CGFloat { size.width }
|
||||
public var height: CGFloat { size.height }
|
||||
|
||||
public subscript(guide: HorizontalAlignment) -> CGFloat {
|
||||
self[explicit: guide] ?? guide.id.defaultValue(in: self)
|
||||
}
|
||||
|
||||
public subscript(guide: VerticalAlignment) -> CGFloat {
|
||||
self[explicit: guide] ?? guide.id.defaultValue(in: self)
|
||||
}
|
||||
|
||||
public subscript(explicit guide: HorizontalAlignment) -> CGFloat? {
|
||||
alignmentGuides[.init(guide.id)]
|
||||
}
|
||||
|
||||
public subscript(explicit guide: VerticalAlignment) -> CGFloat? {
|
||||
alignmentGuides[.init(guide.id)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/3/22.
|
||||
//
|
||||
|
||||
/// A type that can visit a `View`.
|
||||
public protocol ViewVisitor {
|
||||
func visit<V: View>(_ view: V)
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func _visitChildren<V: ViewVisitor>(_ visitor: V) {
|
||||
visitor.visit(body)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ViewVisitorF<V: ViewVisitor> = (V) -> ()
|
||||
|
||||
/// A type that creates a `Result` by visiting multiple `View`s.
|
||||
protocol ViewReducer {
|
||||
associatedtype Result
|
||||
static func reduce<V: View>(into partialResult: inout Result, nextView: V)
|
||||
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result
|
||||
}
|
||||
|
||||
extension ViewReducer {
|
||||
static func reduce<V: View>(into partialResult: inout Result, nextView: V) {
|
||||
partialResult = Self.reduce(partialResult: partialResult, nextView: nextView)
|
||||
}
|
||||
|
||||
static func reduce<V: View>(partialResult: Result, nextView: V) -> Result {
|
||||
var result = partialResult
|
||||
Self.reduce(into: &result, nextView: nextView)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ViewVisitor` that uses a `ViewReducer`
|
||||
/// to collapse the `View` values into a single `Result`.
|
||||
final class ReducerVisitor<R: ViewReducer>: ViewVisitor {
|
||||
var result: R.Result
|
||||
|
||||
init(initialResult: R.Result) {
|
||||
result = initialResult
|
||||
}
|
||||
|
||||
func visit<V>(_ view: V) where V: View {
|
||||
R.reduce(into: &result, nextView: view)
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewReducer {
|
||||
typealias Visitor = ReducerVisitor<Self>
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2022 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 2/11/22.
|
||||
//
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public enum WalkWorkResult<Success> {
|
||||
case `continue`
|
||||
case `break`(with: Success)
|
||||
case pause
|
||||
}
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public enum WalkResult<Renderer: FiberRenderer, Success> {
|
||||
case success(Success)
|
||||
case finished
|
||||
case paused(at: FiberReconciler<Renderer>.Fiber)
|
||||
}
|
||||
|
||||
/// Walk a fiber tree from `root` until the `work` predicate returns `false`.
|
||||
@_spi(TokamakCore)
|
||||
@discardableResult
|
||||
public func walk<Renderer: FiberRenderer>(
|
||||
_ root: FiberReconciler<Renderer>.Fiber,
|
||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> Bool
|
||||
) rethrows -> WalkResult<Renderer, ()> {
|
||||
try walk(root) {
|
||||
try work($0) ? .continue : .pause
|
||||
}
|
||||
}
|
||||
|
||||
/// Parent-first depth-first traversal of a `Fiber` tree.
|
||||
/// `work` is called with each `Fiber` in the tree as they are entered.
|
||||
///
|
||||
/// Traversal uses the following process:
|
||||
/// 1. Perform work on the current `Fiber`.
|
||||
/// 2. If the `Fiber` has a child, repeat from (1) with the child.
|
||||
/// 3. If the `Fiber` does not have a sibling, walk up until we find a `Fiber` that does have one.
|
||||
/// 4. Walk across to the sibling.
|
||||
///
|
||||
/// When the `root` is reached, the loop exits.
|
||||
@_spi(TokamakCore)
|
||||
public func walk<Renderer: FiberRenderer, Success>(
|
||||
_ root: FiberReconciler<Renderer>.Fiber,
|
||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> WalkWorkResult<Success>
|
||||
) rethrows -> WalkResult<Renderer, Success> {
|
||||
var current = root
|
||||
while true {
|
||||
// Perform work on the node
|
||||
switch try work(current) {
|
||||
case .continue: break
|
||||
case let .break(success): return .success(success)
|
||||
case .pause: return .paused(at: current)
|
||||
}
|
||||
// Walk into the child
|
||||
if let child = current.child {
|
||||
current = child
|
||||
continue
|
||||
}
|
||||
// When we walk back to the root, exit
|
||||
if current === root {
|
||||
return .finished
|
||||
}
|
||||
// Walk back up until we find a sibling
|
||||
while current.sibling == nil {
|
||||
// When we walk back to the root, exit
|
||||
guard let parent = current.parent,
|
||||
parent !== root else { return .finished }
|
||||
current = parent
|
||||
}
|
||||
// Walk the sibling
|
||||
current = current.sibling!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public enum ContentMode: Hashable, CaseIterable {
|
||||
case fit
|
||||
case fill
|
||||
}
|
||||
|
||||
public struct _AspectRatioLayout: ViewModifier {
|
||||
public let aspectRatio: CGFloat?
|
||||
public let contentMode: ContentMode
|
||||
|
||||
@inlinable
|
||||
public init(aspectRatio: CGFloat?, contentMode: ContentMode) {
|
||||
self.aspectRatio = aspectRatio
|
||||
self.contentMode = contentMode
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func aspectRatio(
|
||||
_ aspectRatio: CGFloat? = nil,
|
||||
contentMode: ContentMode
|
||||
) -> some View {
|
||||
modifier(
|
||||
_AspectRatioLayout(
|
||||
aspectRatio: aspectRatio,
|
||||
contentMode: contentMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func aspectRatio(
|
||||
_ aspectRatio: CGSize,
|
||||
contentMode: ContentMode
|
||||
) -> some View {
|
||||
self.aspectRatio(
|
||||
aspectRatio.width / aspectRatio.height,
|
||||
contentMode: contentMode
|
||||
)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func scaledToFit() -> some View {
|
||||
aspectRatio(contentMode: .fit)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func scaledToFill() -> some View {
|
||||
aspectRatio(contentMode: .fill)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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,6 +29,11 @@ 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 }
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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,12 +27,21 @@ 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[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/9/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@frozen
|
||||
public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||
public var scale: CGSize
|
||||
public var anchor: UnitPoint
|
||||
|
||||
@inlinable
|
||||
public init(scale: CGSize, anchor: UnitPoint = .center) {
|
||||
self.scale = scale
|
||||
self.anchor = anchor
|
||||
}
|
||||
|
||||
public func effectValue(size: CGSize) -> ProjectionTransform {
|
||||
.init(.init(scaleX: scale.width, y: scale.height))
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View {
|
||||
modifier(_ScaleEffect(scale: scale, anchor: anchor))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some View {
|
||||
scaleEffect(CGSize(width: s, height: s), anchor: anchor)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func scaleEffect(
|
||||
x: CGFloat = 1.0,
|
||||
y: CGFloat = 1.0,
|
||||
anchor: UnitPoint = .center
|
||||
) -> some View {
|
||||
scaleEffect(CGSize(width: x, height: y), anchor: anchor)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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?
|
||||
|
@ -24,11 +26,11 @@ public struct _FlexFrameLayout: ViewModifier {
|
|||
// These are special cases in SwiftUI, where the child
|
||||
// will request the entire width/height of the parent.
|
||||
public var fillWidth: Bool {
|
||||
minWidth == 0 && maxWidth == .infinity
|
||||
(minWidth == 0 || minWidth == nil) && maxWidth == .infinity
|
||||
}
|
||||
|
||||
public var fillHeight: Bool {
|
||||
minHeight == 0 && maxHeight == .infinity
|
||||
(minHeight == 0 || minHeight == nil) && maxHeight == .infinity
|
||||
}
|
||||
|
||||
init(
|
||||
|
@ -54,6 +56,10 @@ public struct _FlexFrameLayout: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension _FlexFrameLayout: Animatable {
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func frame(
|
||||
minWidth: CGFloat? = nil,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 _FrameLayout: ViewModifier {
|
||||
public let width: CGFloat?
|
||||
public let height: CGFloat?
|
||||
|
@ -28,6 +30,10 @@ public struct _FrameLayout: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension _FrameLayout: Animatable {
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func frame(
|
||||
width: CGFloat? = nil,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
/// Underscore is present in the name for SwiftUI compatibility.
|
||||
public struct _HoverActionModifier: ViewModifier {
|
||||
public var hover: ((Bool) -> ())?
|
||||
|
||||
public typealias Body = Never
|
||||
}
|
||||
|
||||
extension ModifiedContent
|
||||
where Content: View, Modifier == _HoverActionModifier
|
||||
{
|
||||
var hover: ((Bool) -> ())? { modifier.hover }
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func onHover(perform action: ((Bool) -> ())?) -> some View {
|
||||
modifier(_HoverActionModifier(hover: action))
|
||||
}
|
||||
}
|
|
@ -14,11 +14,34 @@
|
|||
|
||||
// FIXME: these should have standalone implementations
|
||||
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 }
|
||||
}
|
||||
|
|
|
@ -13,12 +13,16 @@
|
|||
// limitations under the License.
|
||||
|
||||
protocol ModifierContainer {
|
||||
var environmentModifier: EnvironmentModifier? { get }
|
||||
var environmentModifier: _EnvironmentModifier? { get }
|
||||
}
|
||||
|
||||
protocol ModifiedContentProtocol {}
|
||||
|
||||
/// A value with a modifier applied to it.
|
||||
public struct ModifiedContent<Content, Modifier> {
|
||||
@Environment(\.self) public var environment
|
||||
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||
@Environment(\.self)
|
||||
public var environment
|
||||
|
||||
public typealias Body = Never
|
||||
public private(set) var content: Content
|
||||
public private(set) var modifier: Modifier
|
||||
|
@ -30,7 +34,7 @@ public struct ModifiedContent<Content, Modifier> {
|
|||
}
|
||||
|
||||
extension ModifiedContent: ModifierContainer {
|
||||
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
|
||||
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
|
||||
}
|
||||
|
||||
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||
|
@ -39,7 +43,7 @@ extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
|||
}
|
||||
}
|
||||
|
||||
extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier {
|
||||
extension ModifiedContent: View, GroupView, ParentView where Content: View, Modifier: ViewModifier {
|
||||
public var body: Body {
|
||||
neverBody("ModifiedContent<View, ViewModifier>")
|
||||
}
|
||||
|
|
|
@ -23,6 +23,32 @@ public extension View {
|
|||
navigationTitle(title)
|
||||
}
|
||||
|
||||
@available(
|
||||
*,
|
||||
deprecated,
|
||||
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
|
||||
)
|
||||
func navigationBarTitle(
|
||||
_ title: Text,
|
||||
displayMode: NavigationBarItem.TitleDisplayMode
|
||||
) -> some View {
|
||||
navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(displayMode)
|
||||
}
|
||||
|
||||
@available(
|
||||
*,
|
||||
deprecated,
|
||||
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
|
||||
)
|
||||
func navigationBarTitle<S: StringProtocol>(
|
||||
_ title: S,
|
||||
displayMode: NavigationBarItem.TitleDisplayMode
|
||||
) -> some View {
|
||||
navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(displayMode)
|
||||
}
|
||||
|
||||
func navigationTitle(_ title: Text) -> some View {
|
||||
navigationTitle { title }
|
||||
}
|
||||
|
@ -36,4 +62,11 @@ public extension View {
|
|||
{
|
||||
preference(key: NavigationTitleKey.self, value: AnyView(title()))
|
||||
}
|
||||
|
||||
func navigationBarTitleDisplayMode(
|
||||
_ displayMode: NavigationBarItem
|
||||
.TitleDisplayMode
|
||||
) -> some View {
|
||||
preference(key: NavigationBarItemKey.self, value: .init(displayMode: displayMode))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 _PaddingLayout: ViewModifier {
|
||||
public var edges: Edge.Set
|
||||
public var insets: EdgeInsets?
|
||||
|
@ -26,17 +28,36 @@ public struct _PaddingLayout: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
extension _PaddingLayout: Animatable {
|
||||
public typealias AnimatableData = EmptyAnimatableData
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func padding(_ insets: EdgeInsets) -> some View {
|
||||
func padding(_ insets: EdgeInsets) -> ModifiedContent<Self, _PaddingLayout> {
|
||||
modifier(_PaddingLayout(insets: insets))
|
||||
}
|
||||
|
||||
func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
|
||||
func padding(
|
||||
_ edges: Edge.Set = .all,
|
||||
_ length: CGFloat? = nil
|
||||
) -> ModifiedContent<Self, _PaddingLayout> {
|
||||
let insets = length.map { EdgeInsets(_all: $0) }
|
||||
return modifier(_PaddingLayout(edges: edges, insets: insets))
|
||||
}
|
||||
|
||||
func padding(_ length: CGFloat) -> some View {
|
||||
func padding(_ length: CGFloat) -> ModifiedContent<Self, _PaddingLayout> {
|
||||
padding(.all, length)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ModifiedContent where Modifier == _PaddingLayout, Content: View {
|
||||
func padding(_ length: CGFloat) -> ModifiedContent<Content, _PaddingLayout> {
|
||||
var layout = modifier
|
||||
layout.insets?.top += length
|
||||
layout.insets?.leading += length
|
||||
layout.insets?.bottom += length
|
||||
layout.insets?.trailing += length
|
||||
|
||||
return ModifiedContent(content: content, modifier: layout)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,106 @@
|
|||
public struct _ShadowLayout: ViewModifier, EnvironmentReader {
|
||||
// Copyright 2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct _ShadowEffect: EnvironmentalModifier, Equatable {
|
||||
public var color: Color
|
||||
public var radius: CGFloat
|
||||
public var x: CGFloat
|
||||
public var y: CGFloat
|
||||
public var environment: EnvironmentValues!
|
||||
public var offset: CGSize
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
@inlinable
|
||||
init(
|
||||
color: Color,
|
||||
radius: CGFloat,
|
||||
offset: CGSize
|
||||
) {
|
||||
self.color = color
|
||||
self.radius = radius
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
public func resolve(in environment: EnvironmentValues) -> _Resolved {
|
||||
.init(
|
||||
color: color.provider.resolve(in: environment),
|
||||
radius: radius,
|
||||
offset: offset
|
||||
)
|
||||
}
|
||||
|
||||
public struct _Resolved: ViewModifier, Animatable {
|
||||
public var color: AnyColorBox.ResolvedValue
|
||||
public var radius: CGFloat
|
||||
public var offset: CGSize
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
|
||||
public typealias AnimatableData = AnimatablePair<
|
||||
AnimatablePair<
|
||||
Float,
|
||||
AnimatablePair<
|
||||
Float,
|
||||
AnimatablePair<Float, Float>
|
||||
>
|
||||
>,
|
||||
AnimatablePair<CGFloat, CGSize.AnimatableData>
|
||||
>
|
||||
public var animatableData: _Resolved.AnimatableData {
|
||||
get {
|
||||
.init(
|
||||
.init(
|
||||
Float(color.red),
|
||||
.init(
|
||||
Float(color.green),
|
||||
.init(
|
||||
Float(color.blue),
|
||||
Float(color.opacity)
|
||||
)
|
||||
)
|
||||
),
|
||||
.init(radius, offset.animatableData)
|
||||
)
|
||||
}
|
||||
set {
|
||||
color = .init(
|
||||
red: Double(newValue[].0[].0),
|
||||
green: Double(newValue[].0[].1[].0),
|
||||
blue: Double(newValue[].0[].1[].1[].0),
|
||||
opacity: Double(newValue[].0[].1[].1[].1),
|
||||
space: .sRGB
|
||||
)
|
||||
(radius, offset.animatableData) = newValue[].1[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func shadow(
|
||||
color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
|
||||
radius: CGFloat,
|
||||
x: CGFloat = 0,
|
||||
y: CGFloat = 0
|
||||
) -> some View {
|
||||
modifier(_ShadowLayout(color: color, radius: radius, x: x, y: y))
|
||||
modifier(
|
||||
_ShadowEffect(
|
||||
color: color,
|
||||
radius: radius,
|
||||
offset: .init(width: x, height: y)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
// Copyright 2020-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +15,29 @@
|
|||
// Created by Carson Katri on 6/29/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Override this View's body to provide a layout that fits the background to the content.
|
||||
public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
||||
where Content: View, Background: View
|
||||
{
|
||||
public let content: Content
|
||||
public let background: Background
|
||||
public let alignment: Alignment
|
||||
|
||||
@_spi(TokamakCore)
|
||||
public init(content: Content, background: Background, alignment: Alignment) {
|
||||
self.content = content
|
||||
self.background = background
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(background)
|
||||
visitor.visit(content)
|
||||
}
|
||||
}
|
||||
|
||||
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||
where Background: View
|
||||
{
|
||||
|
@ -28,11 +51,11 @@ public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
|||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
// FIXME: Clip to bounds of foreground.
|
||||
ZStack(alignment: alignment) {
|
||||
background
|
||||
content
|
||||
}
|
||||
_BackgroundLayout(
|
||||
content: content,
|
||||
background: background,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
|
@ -56,6 +79,74 @@ public extension View {
|
|||
) -> some View where Background: View {
|
||||
modifier(_BackgroundModifier(background: background, alignment: alignment))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func background<V>(
|
||||
alignment: Alignment = .center,
|
||||
@ViewBuilder content: () -> V
|
||||
) -> some View where V: View {
|
||||
background(content(), alignment: alignment)
|
||||
}
|
||||
}
|
||||
|
||||
@frozen
|
||||
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||
where Style: ShapeStyle, Bounds: Shape
|
||||
{
|
||||
public var environment: EnvironmentValues!
|
||||
|
||||
public var style: Style
|
||||
public var shape: Bounds
|
||||
public var fillStyle: FillStyle
|
||||
|
||||
@inlinable
|
||||
public init(style: Style, shape: Bounds, fillStyle: FillStyle) {
|
||||
self.style = style
|
||||
self.shape = shape
|
||||
self.fillStyle = fillStyle
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.background(shape.fill(style, style: fillStyle))
|
||||
}
|
||||
|
||||
public mutating func setContent(from values: EnvironmentValues) {
|
||||
environment = values
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func background<S, T>(
|
||||
_ style: S,
|
||||
in shape: T,
|
||||
fillStyle: FillStyle = FillStyle()
|
||||
) -> some View where S: ShapeStyle, T: Shape {
|
||||
modifier(_BackgroundShapeModifier(style: style, shape: shape, fillStyle: fillStyle))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func background<S>(
|
||||
in shape: S,
|
||||
fillStyle: FillStyle = FillStyle()
|
||||
) -> some View where S: Shape {
|
||||
background(BackgroundStyle(), in: shape, fillStyle: fillStyle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Override this View's body to provide a layout that fits the background to the content.
|
||||
public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
||||
where Content: View, Overlay: View
|
||||
{
|
||||
public let content: Content
|
||||
public let overlay: Overlay
|
||||
public let alignment: Alignment
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitor.visit(content)
|
||||
visitor.visit(overlay)
|
||||
}
|
||||
}
|
||||
|
||||
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||
|
@ -71,11 +162,11 @@ public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
|||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
// FIXME: Clip to content shape.
|
||||
ZStack(alignment: alignment) {
|
||||
content
|
||||
overlay
|
||||
}
|
||||
_OverlayLayout(
|
||||
content: content,
|
||||
overlay: overlay,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
mutating func setContent(from values: EnvironmentValues) {
|
||||
|
@ -96,6 +187,21 @@ public extension View {
|
|||
modifier(_OverlayModifier(overlay: overlay, alignment: alignment))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func overlay<V>(
|
||||
alignment: Alignment = .center,
|
||||
@ViewBuilder content: () -> V
|
||||
) -> some View where V: View {
|
||||
modifier(_OverlayModifier(overlay: content(), alignment: alignment))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func overlay<S>(
|
||||
_ style: S
|
||||
) -> some View where S: ShapeStyle {
|
||||
overlay(Rectangle().fill(style))
|
||||
}
|
||||
|
||||
func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle {
|
||||
overlay(Rectangle().strokeBorder(content, lineWidth: width))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
public extension View {
|
||||
func task(
|
||||
priority: TaskPriority = .userInitiated,
|
||||
_ action: @escaping @Sendable () async -> ()
|
||||
) -> some View {
|
||||
var task: Task<(), Never>?
|
||||
return onAppear {
|
||||
task = Task(priority: priority, operation: action)
|
||||
}
|
||||
.onDisappear {
|
||||
task?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,20 +16,51 @@ public protocol ViewModifier {
|
|||
typealias Content = _ViewModifier_Content<Self>
|
||||
associatedtype Body: View
|
||||
func body(content: Content) -> Self.Body
|
||||
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor
|
||||
}
|
||||
|
||||
public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier {
|
||||
public extension ViewModifier {
|
||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||
.init(inputs: inputs)
|
||||
}
|
||||
|
||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
||||
if Body.self == Never.self {
|
||||
content.visitChildren(visitor)
|
||||
} else {
|
||||
visitor.visit(body(content: content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct _ViewModifier_Content<Modifier>: View
|
||||
where Modifier: ViewModifier
|
||||
{
|
||||
public let modifier: Modifier
|
||||
public let view: AnyView
|
||||
let visitChildren: (ViewVisitor) -> ()
|
||||
|
||||
public init(modifier: Modifier, view: AnyView) {
|
||||
self.modifier = modifier
|
||||
self.view = view
|
||||
visitChildren = { $0.visit(view) }
|
||||
}
|
||||
|
||||
public var body: AnyView {
|
||||
public init<V: View>(modifier: Modifier, view: V) {
|
||||
self.modifier = modifier
|
||||
self.view = AnyView(view)
|
||||
visitChildren = { $0.visit(view) }
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
view
|
||||
}
|
||||
|
||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
||||
visitChildren(visitor)
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
|
|
|
@ -15,33 +15,51 @@
|
|||
// Created by Carson Katri on 7/19/20.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
// This is very similar to `MountedCompositeView`. However, the `mountedBody`
|
||||
// is the computed content of the specified `Scene`, instead of having child
|
||||
// `View`s
|
||||
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
||||
override func mount(
|
||||
before _: R.TargetType? = nil,
|
||||
on _: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
// `App` elements have no siblings, hence the `before` argument is discarded.
|
||||
// They also have no parents, so the `parent` argument is discarded as well.
|
||||
let childBody = reconciler.render(mountedApp: self)
|
||||
|
||||
let child: MountedElement<R> = mountChild(childBody)
|
||||
let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: nil, on: self, with: reconciler)
|
||||
child.transaction = transaction
|
||||
child.mount(before: nil, on: self, in: reconciler, with: transaction)
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
mountedChildren
|
||||
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||
}
|
||||
|
||||
private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> {
|
||||
/// Mounts a child scene within the app.
|
||||
/// - Parameters:
|
||||
/// - renderer: An instance conforming to the `Renderer` protocol to render the mounted
|
||||
/// scene with.
|
||||
/// - childBody: The body of the child scene to mount for this app.
|
||||
/// - Returns: Returns an instance of the `MountedScene` class that's already mounted in this app.
|
||||
private func mountChild(_ renderer: R, _ childBody: _AnyScene) -> MountedScene<R> {
|
||||
let mountedScene: MountedScene<R> = childBody
|
||||
.makeMountedScene(parentTarget, environmentValues, self)
|
||||
.makeMountedScene(renderer, parentTarget, environmentValues, self)
|
||||
if let title = mountedScene.title {
|
||||
// swiftlint:disable force_cast
|
||||
(app.type as! _TitledApp.Type)._setTitle(title)
|
||||
|
@ -49,17 +67,19 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
|||
return mountedScene
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
let element = reconciler.render(mountedApp: self)
|
||||
reconciler.reconcile(
|
||||
self,
|
||||
with: element,
|
||||
transaction: transaction,
|
||||
getElementType: { $0.type },
|
||||
updateChild: {
|
||||
$0.environmentValues = environmentValues
|
||||
$0.scene = _AnyScene(element)
|
||||
$0.transaction = transaction
|
||||
},
|
||||
mountChild: { mountChild($0) }
|
||||
mountChild: { mountChild(reconciler.renderer, $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2020 Tokamak contributors
|
||||
// Copyright 2018-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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
|
||||
|
||||
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
||||
let parentTarget: R.TargetType
|
||||
|
@ -61,10 +61,11 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
|||
_ view: AnyView,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
self.parentTarget = parentTarget
|
||||
super.init(view, environmentValues, parent)
|
||||
super.init(view, environmentValues, viewTraits, parent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,25 +15,43 @@
|
|||
// Created by Max Desiatov on 03/12/2018.
|
||||
//
|
||||
|
||||
import CombineShim
|
||||
import OpenCombineShim
|
||||
|
||||
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
var transaction = transaction
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
// Disable animations on mount so `animation(_:)` doesn't try to animate
|
||||
// until the transition finishes.
|
||||
transaction.disablesAnimations = true
|
||||
self.transaction = transaction
|
||||
|
||||
updateVariadicView()
|
||||
|
||||
let childBody = reconciler.render(compositeView: self)
|
||||
|
||||
if let traitModifier = view.view as? _TraitWritingModifierProtocol {
|
||||
traitModifier.modifyViewTraitStore(&viewTraits)
|
||||
}
|
||||
let child: MountedElement<R> = childBody.makeMountedView(
|
||||
reconciler.renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
viewTraits,
|
||||
self
|
||||
)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: sibling, on: self, with: reconciler)
|
||||
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||
|
||||
// `_TargetRef` is a composite view, so it's enough to check for it only here
|
||||
// `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite
|
||||
// view, so it's enough check for it only here.
|
||||
if var targetRef = view.view as? TargetRefType {
|
||||
// `_TargetRef` body is not always a host view that has a target, need to traverse
|
||||
// all descendants to find a `MountedHostView<R>` instance.
|
||||
|
@ -51,7 +69,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
reconciler.afterCurrentRender(perform: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
|
||||
// FIXME: this has to be implemented in a renderer-specific way, otherwise it's equivalent to
|
||||
// `_onMount` and `_onUnmount` at the moment,
|
||||
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
|
||||
if let appearanceAction = self.view.view as? AppearanceActionType {
|
||||
|
@ -60,36 +78,104 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
|
||||
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
||||
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
||||
if let parent = parent {
|
||||
parent.preferenceStore.merge(with: self.preferenceStore)
|
||||
}
|
||||
}
|
||||
|
||||
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
||||
preferenceReader.preferenceStore(self.preferenceStore)
|
||||
}
|
||||
})
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
|
||||
var transaction = transaction
|
||||
transaction.disablesAnimations = false
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
|
||||
mountedChildren.forEach {
|
||||
$0.viewTraits = self.viewTraits
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
}
|
||||
|
||||
if let appearanceAction = view.view as? AppearanceActionType {
|
||||
appearanceAction.disappear?()
|
||||
}
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
var transaction = transaction
|
||||
transaction.disablesAnimations = false
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
updateVariadicView()
|
||||
let element = reconciler.render(compositeView: self)
|
||||
reconciler.reconcile(
|
||||
self,
|
||||
with: element,
|
||||
transaction: transaction,
|
||||
getElementType: { $0.type },
|
||||
updateChild: {
|
||||
$0.environmentValues = environmentValues
|
||||
$0.view = AnyView(element)
|
||||
$0.transaction = transaction
|
||||
},
|
||||
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
|
||||
mountChild: {
|
||||
$0.makeMountedView(
|
||||
reconciler.renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
viewTraits,
|
||||
self
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if let lifecycleActions = view.view as? LifecycleActionType {
|
||||
lifecycleActions.update?()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVariadicView() {
|
||||
if var tree = view.view as? _VariadicView_AnyTree {
|
||||
let elements = ((tree.anyContent.view as? GroupView)?.recursiveChildren ?? [tree.anyContent])
|
||||
.enumerated()
|
||||
.map { (pair: EnumeratedSequence<[AnyView]>.Element) -> _VariadicView_Children.Element in
|
||||
var viewTraits = _ViewTraitStore(values: [:])
|
||||
if let traitModifier = pair.element.view as? _TraitWritingModifierProtocol {
|
||||
traitModifier.modifyViewTraitStore(&viewTraits)
|
||||
}
|
||||
return _VariadicView_Children.Element(
|
||||
view: pair.element,
|
||||
id: AnyHashable(pair.offset),
|
||||
// TODO: Retrieve the ID from the `IDView`. Maybe this should use traits too.
|
||||
viewTraits: viewTraits,
|
||||
onTraitsUpdated: { _ in }
|
||||
)
|
||||
}
|
||||
tree.children = _VariadicView_Children(elements: elements)
|
||||
view.view = tree
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension GroupView {
|
||||
var recursiveChildren: [AnyView] {
|
||||
var allChildren = [AnyView]()
|
||||
for child in children {
|
||||
if !(child.view is ModifiedContentProtocol),
|
||||
let group = child.view as? GroupView
|
||||
{
|
||||
allChildren.append(contentsOf: group.recursiveChildren)
|
||||
} else {
|
||||
allChildren.append(child)
|
||||
}
|
||||
}
|
||||
return allChildren
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,35 +85,51 @@ public class MountedElement<R: Renderer> {
|
|||
}
|
||||
|
||||
var mountedChildren = [MountedElement<R>]()
|
||||
var environmentValues: EnvironmentValues
|
||||
|
||||
unowned var parent: MountedElement<R>?
|
||||
/// `didSet` on this field propagates the preference changes up the view tree.
|
||||
var preferenceStore: _PreferenceStore = .init() {
|
||||
didSet {
|
||||
parent?.preferenceStore.merge(with: preferenceStore)
|
||||
}
|
||||
}
|
||||
public var transaction: Transaction = .init(animation: nil)
|
||||
/// Where this element is the process of mounting/unmounting.
|
||||
var transitionPhase = TransitionPhase.willMount
|
||||
/// The current `UnmountTask` of this element.
|
||||
var unmountTask: UnmountTask<R>?
|
||||
|
||||
public internal(set) var environmentValues: EnvironmentValues
|
||||
|
||||
private(set) weak var parent: MountedElement<R>?
|
||||
|
||||
var preferenceStore: _PreferenceStore = .init()
|
||||
|
||||
public internal(set) var viewTraits: _ViewTraitStore
|
||||
|
||||
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
element = .app(app)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
element = .scene(scene)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
init(
|
||||
_ view: AnyView,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
element = .view(view)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
self.viewTraits = viewTraits
|
||||
updateEnvironment()
|
||||
connectParentPreferenceStore()
|
||||
}
|
||||
|
||||
func updateEnvironment() {
|
||||
|
@ -128,19 +144,75 @@ public class MountedElement<R: Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
func connectParentPreferenceStore() {
|
||||
preferenceStore.parent = parent?.preferenceStore
|
||||
}
|
||||
|
||||
/// You must call `super.prepareForMount` before all other mounting work.
|
||||
func prepareForMount(with transaction: Transaction) {
|
||||
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||
if case let .view(view) = element,
|
||||
view.type is GroupView.Type
|
||||
{
|
||||
transitionPhase = parent?.transitionPhase ?? .normal
|
||||
}
|
||||
// Allow the root of a mount to transition
|
||||
// (if their parent isn't mounting, then they are the root of the mount).
|
||||
if parent?.transitionPhase == .normal {
|
||||
viewTraits.insert(
|
||||
transaction.animation != nil
|
||||
|| _AnyTransitionProxy(viewTraits.transition)
|
||||
.resolve(in: environmentValues)
|
||||
.insertionAnimation != nil,
|
||||
forKey: CanTransitionTraitKey.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// You must call `super.mount` after all other mounting work.
|
||||
func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
// Set the phase to `normal` after finished mounting.
|
||||
transitionPhase = .normal
|
||||
}
|
||||
|
||||
func unmount(with reconciler: StackReconciler<R>) {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
/// You must call `super.unmount` before all other unmounting work.
|
||||
func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
if !(self is MountedHostView<R>) {
|
||||
unmountTask = parentTask?.appendChild()
|
||||
}
|
||||
|
||||
// `GroupView`'s don't really unmount, so let their children transition if the group can.
|
||||
if case let .view(view) = element,
|
||||
view.type is GroupView.Type
|
||||
{
|
||||
transitionPhase = parent?.transitionPhase ?? .normal
|
||||
} else {
|
||||
// Set the phase to `willUnmount` before unmounting.
|
||||
transitionPhase = .willUnmount
|
||||
}
|
||||
// Allow the root of an unmount to transition
|
||||
// (if their parent isn't unmounting, then they are the root of the unmount).
|
||||
if parent?.transitionPhase == .normal {
|
||||
viewTraits.insert(
|
||||
transaction.animation != nil
|
||||
|| _AnyTransitionProxy(viewTraits.transition)
|
||||
.resolve(in: environmentValues)
|
||||
.removalAnimation != nil,
|
||||
forKey: CanTransitionTraitKey.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func update(with reconciler: StackReconciler<R>) {
|
||||
func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
}
|
||||
|
||||
|
@ -222,16 +294,18 @@ extension TypeInfo {
|
|||
|
||||
extension AnyView {
|
||||
func makeMountedView<R: Renderer>(
|
||||
_ renderer: R,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) -> MountedElement<R> {
|
||||
if type == EmptyView.self {
|
||||
return MountedEmptyView(self, environmentValues, parent)
|
||||
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
|
||||
return MountedHostView(self, parentTarget, environmentValues, parent)
|
||||
return MountedEmptyView(self, environmentValues, viewTraits, parent)
|
||||
} else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
|
||||
return MountedHostView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||
} else {
|
||||
return MountedCompositeView(self, parentTarget, environmentValues, parent)
|
||||
return MountedCompositeView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,20 @@ final class MountedEmptyView<R: Renderer>: MountedElement<R> {
|
|||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
) {}
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {}
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {}
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction?) {}
|
||||
}
|
||||
|
|
|
@ -33,19 +33,25 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
_ view: AnyView,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
self.parentTarget = parentTarget
|
||||
|
||||
super.init(view, environmentValues, parent)
|
||||
super.init(view, environmentValues, viewTraits, parent)
|
||||
}
|
||||
|
||||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
guard let target = reconciler.renderer?.mountTarget(
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
self.transaction = transaction
|
||||
|
||||
guard let target = reconciler.renderer.mountTarget(
|
||||
before: sibling,
|
||||
to: parentTarget,
|
||||
with: self
|
||||
|
@ -56,8 +62,17 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
|
||||
guard !view.children.isEmpty else { return }
|
||||
|
||||
let isGroupView = view.type is GroupView.Type
|
||||
// Don't allow children to transition their mounting since they aren't individually
|
||||
// appearing (unless its a `GroupView`, which is flattened).
|
||||
mountedChildren = view.children.map {
|
||||
$0.makeMountedView(target, environmentValues, self)
|
||||
$0.makeMountedView(
|
||||
reconciler.renderer,
|
||||
target,
|
||||
environmentValues,
|
||||
isGroupView ? self.viewTraits : .init(),
|
||||
self
|
||||
)
|
||||
}
|
||||
|
||||
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
|
||||
|
@ -65,44 +80,73 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
|
||||
`GroupView`.
|
||||
*/
|
||||
let isGroupView = view.type is GroupView.Type
|
||||
mountedChildren.forEach {
|
||||
$0.mount(before: isGroupView ? sibling : nil, on: self, with: reconciler)
|
||||
$0.mount(before: isGroupView ? sibling : nil, on: self, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
private var parentUnmountTask = UnmountTask<R>()
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
|
||||
guard let target = target else { return }
|
||||
|
||||
reconciler.renderer?.unmount(
|
||||
let task = UnmountHostTask(self, in: reconciler) {
|
||||
self.mountedChildren.forEach {
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: self.unmountTask)
|
||||
}
|
||||
}
|
||||
task.isCancelled = parentTask?.isCancelled ?? false
|
||||
unmountTask = task
|
||||
parentTask?.childTasks.append(task)
|
||||
reconciler.renderer.unmount(
|
||||
target: target,
|
||||
from: parentTarget,
|
||||
with: self
|
||||
) {
|
||||
self.mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
}
|
||||
with: task
|
||||
)
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
/// Stop any unfinished unmounts and complete them without transitions.
|
||||
private func invalidateUnmount() {
|
||||
parentUnmountTask.cancel()
|
||||
parentUnmountTask.completeImmediately()
|
||||
parentUnmountTask = .init()
|
||||
}
|
||||
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
guard let target = target else { return }
|
||||
|
||||
invalidateUnmount()
|
||||
|
||||
updateEnvironment()
|
||||
target.view = view
|
||||
reconciler.renderer?.update(target: target, with: self)
|
||||
reconciler.renderer.update(target: target, with: self)
|
||||
|
||||
var childrenViews = view.children
|
||||
|
||||
let traits = view.type is GroupView.Type ? viewTraits : .init()
|
||||
|
||||
switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
|
||||
// if existing children present and new children array is empty
|
||||
// then unmount all existing children
|
||||
case (false, true):
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
mountedChildren.forEach {
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: self.parentUnmountTask)
|
||||
}
|
||||
mountedChildren = []
|
||||
|
||||
// if no existing children then mount all new children
|
||||
case (true, false):
|
||||
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) }
|
||||
mountedChildren.forEach { $0.mount(on: self, with: reconciler) }
|
||||
mountedChildren = childrenViews.map {
|
||||
$0.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||
}
|
||||
mountedChildren.forEach { $0.mount(on: self, in: reconciler, with: transaction) }
|
||||
|
||||
// if both arrays have items then reconcile by types and keys
|
||||
case (false, false):
|
||||
|
@ -117,16 +161,24 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
mountedChild.environmentValues = environmentValues
|
||||
mountedChild.view = childView
|
||||
mountedChild.updateEnvironment()
|
||||
mountedChild.update(with: reconciler)
|
||||
mountedChild.update(in: reconciler, with: transaction)
|
||||
newChild = mountedChild
|
||||
} else {
|
||||
/* note the order of operations here: we mount the new child first, use the mounted child
|
||||
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
|
||||
by unmounting it.
|
||||
*/
|
||||
newChild = childView.makeMountedView(target, environmentValues, self)
|
||||
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler)
|
||||
mountedChild.unmount(with: reconciler)
|
||||
newChild = childView.makeMountedView(
|
||||
reconciler.renderer,
|
||||
target,
|
||||
environmentValues,
|
||||
traits,
|
||||
self
|
||||
)
|
||||
newChild.mount(
|
||||
before: mountedChild.firstDescendantTarget, on: self, in: reconciler, with: transaction
|
||||
)
|
||||
mountedChild.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||
}
|
||||
newChildren.append(newChild)
|
||||
mountedChildren.removeFirst()
|
||||
|
@ -137,15 +189,15 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
// unmount remaining `mountedChildren`
|
||||
if !mountedChildren.isEmpty {
|
||||
for child in mountedChildren {
|
||||
child.unmount(with: reconciler)
|
||||
child.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||
}
|
||||
} else {
|
||||
// more views left than children were mounted,
|
||||
// mount remaining views
|
||||
for firstChild in childrenViews {
|
||||
let newChild: MountedElement<R> =
|
||||
firstChild.makeMountedView(target, environmentValues, self)
|
||||
newChild.mount(on: self, with: reconciler)
|
||||
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||
newChild.mount(on: self, in: reconciler, with: transaction)
|
||||
newChildren.append(newChild)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,25 +31,36 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
|||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
let childBody = reconciler.render(mountedScene: self)
|
||||
|
||||
let child: MountedElement<R> = childBody
|
||||
.makeMountedElement(parentTarget, environmentValues, self)
|
||||
.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: sibling, on: self, with: reconciler)
|
||||
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
mountedChildren
|
||||
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
let element = reconciler.render(mountedScene: self)
|
||||
reconciler.reconcile(
|
||||
self,
|
||||
with: element,
|
||||
transaction: transaction,
|
||||
getElementType: { $0.type },
|
||||
updateChild: {
|
||||
$0.environmentValues = environmentValues
|
||||
|
@ -59,8 +70,11 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
|||
case let .view(view):
|
||||
$0.view = AnyView(view)
|
||||
}
|
||||
$0.transaction = transaction
|
||||
},
|
||||
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) }
|
||||
mountChild: {
|
||||
$0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -76,21 +90,23 @@ extension _AnyScene.BodyResult {
|
|||
}
|
||||
|
||||
func makeMountedElement<R: Renderer>(
|
||||
_ renderer: R,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ parent: MountedElement<R>?
|
||||
) -> MountedElement<R> {
|
||||
switch self {
|
||||
case let .scene(scene):
|
||||
return scene.makeMountedScene(parentTarget, environmentValues, parent)
|
||||
return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent)
|
||||
case let .view(view):
|
||||
return view.makeMountedView(parentTarget, environmentValues, parent)
|
||||
return view.makeMountedView(renderer, parentTarget, environmentValues, .init(), parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _AnyScene {
|
||||
func makeMountedScene<R: Renderer>(
|
||||
_ renderer: R,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ parent: MountedElement<R>?
|
||||
|
@ -104,11 +120,17 @@ extension _AnyScene {
|
|||
let children: [MountedElement<R>]
|
||||
if let deferredScene = scene as? SceneDeferredToRenderer {
|
||||
children = [
|
||||
deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent),
|
||||
deferredScene.deferredBody.makeMountedView(
|
||||
renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
.init(),
|
||||
parent
|
||||
),
|
||||
]
|
||||
} else if let groupScene = scene as? GroupScene {
|
||||
children = groupScene.children.map {
|
||||
$0.makeMountedScene(parentTarget, environmentValues, parent)
|
||||
$0.makeMountedScene(renderer, parentTarget, environmentValues, parent)
|
||||
}
|
||||
} else {
|
||||
children = []
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2018-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/19/21.
|
||||
//
|
||||
|
||||
/// A tree of cancellable in-progress unmounts.
|
||||
public class UnmountTask<R> where R: Renderer {
|
||||
public internal(set) var isCancelled = false
|
||||
var childTasks = [UnmountTask<R>]()
|
||||
private let callback: () -> ()
|
||||
|
||||
init(_ callback: @escaping () -> () = {}) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
forEach { $0.isCancelled = true }
|
||||
}
|
||||
|
||||
/// Call after completely unmounting the `host`.
|
||||
public func finish() {
|
||||
callback()
|
||||
}
|
||||
|
||||
/// Adds and returns a new child `UnmountTask`
|
||||
func appendChild() -> UnmountTask<R> {
|
||||
let child = UnmountTask()
|
||||
child.isCancelled = isCancelled
|
||||
childTasks.append(child)
|
||||
return child
|
||||
}
|
||||
|
||||
/// Forces the element and all child tasks to unmount without transition.
|
||||
func completeImmediately() {
|
||||
forEach {
|
||||
guard $0 is UnmountHostTask<R> else { return }
|
||||
$0.completeImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
func forEach(_ f: (UnmountTask<R>) -> ()) {
|
||||
var stack = [self]
|
||||
while let last = stack.popLast() {
|
||||
f(last)
|
||||
stack.insert(contentsOf: last.childTasks, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state for the unmounting of a `MountedHostView` by a `Renderer`.
|
||||
public final class UnmountHostTask<R>: UnmountTask<R> where R: Renderer {
|
||||
public private(set) weak var host: MountedHostView<R>!
|
||||
private unowned var reconciler: StackReconciler<R>
|
||||
|
||||
init(
|
||||
_ host: MountedHostView<R>,
|
||||
in reconciler: StackReconciler<R>,
|
||||
callback: @escaping () -> ()
|
||||
) {
|
||||
self.host = host
|
||||
self.reconciler = reconciler
|
||||
super.init(callback)
|
||||
}
|
||||
|
||||
override func completeImmediately() {
|
||||
host.viewTraits.insert(false, forKey: CanTransitionTraitKey.self)
|
||||
host.unmount(in: reconciler, with: .init(animation: nil), parentTask: nil)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue