Compare commits

...

33 Commits
v0.10 ... main

Author SHA1 Message Date
dnrops eb50bca9f7 Update Package.swift 2024-08-27 20:58:20 +08:00
dnrops cf1304ef90 Update Package.swift 2024-08-27 20:51:20 +08:00
Lukas e0d8e9db46
fix replaceChild parameter order (#522) 2023-02-04 17:31:26 -05:00
Greg Cotten f1cbfcf073
[bug] Missing CoreGraphics import for Swift 5.7.1 / Xcode 14.1 (#528) 2022-11-10 22:12:25 -05:00
Lukas 56822c906b
Enable async/await in DOM Fiber renderer (#516)
I noticed that Tasks weren't running when using the Fiber renderer. I'm
not sure this is the appropriate place, but the normal DOM renderer
calls `installGlobalExecutor` in it's `init`, so I just mirrored that.
2022-10-02 15:36:29 -04:00
Max Desiatov af810902bd
Create `FAQ.md` (#511)
This is a document we could redirect people to with questions about our layout system, naming, and history of the project. Feel free to expand this with more relevant topics.

Co-authored-by: Carson Katri <Carson.katri@gmail.com>
2022-09-02 12:47:17 +00:00
BenedictSt b24b964fd7
Fix typos in doc comments across the codebase (#513) 2022-09-02 13:43:54 +01:00
Max Desiatov e7e318e64e
Disable GTK macOS builds (#512)
Errors started cropping up during CI runs on https://github.com/TokamakUI/Tokamak/pull/511. I think it's better to disable this job until we find a good way to fix it. It wasn't a job required for merging PRs anyway, as GTK renderer is currently experimental.
2022-09-02 13:43:08 +01:00
Max Desiatov 47c5a05068
Update default assignee in `bug_report.md` (#510) 2022-08-29 10:23:03 +01:00
Max Desiatov fa0a8d2447
Update maintainers list in `README.md` 2022-08-22 19:00:26 +01:00
Max Desiatov 687baee97f
Use Swift 5.7 nightly on CI, fix build issues (#507)
* Use Swift 5.7 nightly on CI to fix build issues

* Update SwiftWasm snapshots

* Remove initializers that became ambiguous in 5.7
2022-07-30 11:26:20 +02:00
Max Desiatov 10d9d32b97
Update maintainers list in `README.md` 2022-07-26 14:32:44 +01:00
Carson Katri 4e8b84e4a1
Allow window resizing in Fiber renderers (#502)
* Add publisher for sceneSize

* Resolve build issue

* Add documentation

* Fix double update issue
2022-07-06 08:45:49 -04:00
Carson Katri 676760d34b
Add support for preferences, `@StateObject`, `@EnvironmentObject`, and custom `DynamicProperty` types (#501)
* Pass preferences up the Fiber tree

* Working preferences (except for backgroundPreferenceValue)

* Initial StateObject support

* Fix layout caching bug

* Support StateObject/EnvironmentObject/custom DynamicProperty

* Add doc comments for bindProperties and updateDynamicProperties

* Use immediate scheduling in static HTML and test renderers

* Add preferences test

* Add state tests and improve testing API

* Attempt to fix tests

* Fix preference tests

* Attempt to fix tests when Dispatch is unavailable

* #if out on os(WASI)

* Add check for WASI to test

* Add check for WASI to TestViewProxy

* #if out of import Dispatch when os == WASI

* Remove all Dispatch code

* Remove address from debugDescription

* Move TestViewProxy to TokamakTestRenderer

* Add memory address to Fiber.debugDescription

* Fix copyright year

* Add several missing types to Core.swift

* Add missing LayoutValueKey to Core.swift

* Fix issues with view trait propagation

* Enable App/SceneStorage in DOMFiberRenderer

* Address review comments

* Revise preference implementation
2022-07-05 18:04:28 -04:00
Carson Katri 9d0e2fc067
Support `Image` in Fiber renderers (#500)
* Support image and aspect ratio

* Fix measureImage implementation in test renderer

* Improve accuracy of aspectRatio

* Rename intrinsicSize to hide from autocomplete
2022-06-28 20:04:42 +00:00
Carson Katri d78ab20ea8
Add `Layout` protocol for FiberReconciler (#498)
* Initial pass at Layout protocol implementation

* Move layout into a separate pass

* Split reconciler into separate FiberReconcilerPass-es

* Cleanup reconcile pass

* Simplify cache implementation

* Optimize array capacity and persist cache between runs

* Revert viewChildrenCount

* Improve accuracy of stack layout

* Try caching sizeThatFits

* Revise caching of sizeThatFits

* Cleanup layout files and split up

* Further cleanup

* Add ContainedZLayout

* Add frame layouts

* Add snapshots tests that compare against native SwiftUI

* Perform updates from the top of the view hierarchy for dynamic layout

* Add Package.swift

* Fix reconciler bug

* Add test case for reconciler insert bug

* Respect spacing preferences

* Revise cache and spacing logic to match SwiftUI default implementations

* Allow spacing changes based on adjacent View type

* Support view traits in FiberReconciler (and LayoutValueKey by extension)

* Propagate cache invalidation

* Cleanup attributes

* Simplify LayoutPass and improve accuracy

* Cleanup logs

* Add layoutPriority tests

* Revise conflict with main

* Dictionary performance catch

* Remove unneccesary capacity preservation

* Update TokamakCoreBenchmark to handle LayoutView addition at hierarchy root

* Implement AnyLayout and replace LayoutActions

* Allow VStack/HStack to be created without children

* Fix padding implementation

* Formatting fixes

* Space out ViewArguments.swift properties

* Add backing storage to LayoutSubview to move out of ReconcilePass and slightly optimize stack usage
2022-06-27 08:53:30 -04:00
Yuta Saito 5e6fe2d2c9
Fix build and tests for 5.7 toolchain (#499) 2022-06-26 03:42:37 +09:00
filip-sakel c4717d5cae
Implement `AnyShape` (#497)
* Implement AnyShape.

* Remove unnecessary body implementation.
2022-06-24 08:38:14 -04:00
ezraberch 0b182d99a1
Add file size to benchmark script (#496)
* * Add file size to benchmark script
* Reenable macOS TokamakDemo build in CI

* Update benchmark.sh

* Add Wasm size annotation

* Make sizes more readable
2022-06-24 08:36:35 -04:00
Carson Katri 546b9e572f
Add `_PaddingLayout` support to layout engine (#485)
* Initial Reconciler using visitor pattern

* Preliminary static HTML renderer using the new reconciler

* Add environment

* Initial DOM renderer

* Nearly-working and simplified reconciler

* Working reconciler for HTML/DOM renderers

* Rename files, and split code across files

* Add some documentation and refinements

* Remove GraphRendererTests

* Initial layout engine (only implemented for the TestRenderer)

* Layout engine for the DOM renderer

* Refined layout pass

* Revise positioning and restoration of position styles on .update

* Re-add Optional.body for StackReconciler-based renderers

* Add text measurement

* Add spacing to StackLayout

* Add benchmarks to compare the stack/fiber reconcilers

* Fix some issues created for the StackReconciler, and add update benchmarks

* Add BenchmarkState.measure to only calculate the time to update

* Fix hang in update shallow benchmark

* Fix build errors

* Address build issues

* Remove File.swift headers

* Rename Element -> FiberElement and Element.Data -> FiberElement.Content

* Add doc comment explaining unowned usage

* Add doc comments explaining implicitly unwrapped optionals

* Attempt to use Swift instead of JS for applying mutations

* Fix issue with not applying updates to DOMFiberElement

* Add comment explaining manual implementation of Hashable for PropertyInfo

* Fix linter issues

* Remove dynamicMember label from subscript

* Re-enable carton test

* Attempt GTK fix

* Add option to disable layout in the FiberReconciler

* Re-enable TokamakDemo with StackReconciler

* Restore CI config

* Restore CI config

* Add file headers and cleanup structure

* Add 'px' to font-size in test outputs

* Remove extra newlines

* Keep track of 'elementChildren' so children are positioned in the correct order

* Use a ViewVisitor to pass the correct View type to the proposeSize function

* Add support for view modifiers

* Add frame modifier to demonstrate modifiers

* Fix TestRenderer

* Remove unused property

* Add PaddingLayoutComputer

* Fix doc comment

* Fix doc comment

* Fix linter issues and refactor slightly

* Fix benchmark builds

* Attempt to fix benchmarks

* Fix sibling layout issues

* Restore original demo

* Address review comments

* Remove maxAxis and fitAxis properties

* Use switch instead of ternary operators

* Add more documentation to layout steps

* Resolve reconciler issue due to alternate child not being cleared/released

* Apply suggestions from code review

Co-authored-by: Max Desiatov <max@desiatov.com>

* Reuse Text resolution code.

* Add more documentation

* Fix typo

* Use structs for LayoutComputers

* Update AlignmentID demo

* Fix weird formatting

* Fix copyright year

* Restore original demo

* Move attribute to newline

* Address review comments

Co-authored-by: Max Desiatov <max@desiatov.com>
2022-06-15 10:34:13 -04:00
Carson Katri c935744ae8
Add `_ShapeView` and `background` modifiers support to Fiber renderers (#491)
* Initial Reconciler using visitor pattern

* Preliminary static HTML renderer using the new reconciler

* Add environment

* Initial DOM renderer

* Nearly-working and simplified reconciler

* Working reconciler for HTML/DOM renderers

* Rename files, and split code across files

* Add some documentation and refinements

* Remove GraphRendererTests

* Initial layout engine (only implemented for the TestRenderer)

* Layout engine for the DOM renderer

* Refined layout pass

* Revise positioning and restoration of position styles on .update

* Re-add Optional.body for StackReconciler-based renderers

* Add text measurement

* Add spacing to StackLayout

* Add benchmarks to compare the stack/fiber reconcilers

* Fix some issues created for the StackReconciler, and add update benchmarks

* Add BenchmarkState.measure to only calculate the time to update

* Fix hang in update shallow benchmark

* Fix build errors

* Address build issues

* Remove File.swift headers

* Rename Element -> FiberElement and Element.Data -> FiberElement.Content

* Add doc comment explaining unowned usage

* Add doc comments explaining implicitly unwrapped optionals

* Attempt to use Swift instead of JS for applying mutations

* Fix issue with not applying updates to DOMFiberElement

* Add comment explaining manual implementation of Hashable for PropertyInfo

* Fix linter issues

* Remove dynamicMember label from subscript

* Re-enable carton test

* Attempt GTK fix

* Add option to disable layout in the FiberReconciler

* Re-enable TokamakDemo with StackReconciler

* Restore CI config

* Restore CI config

* Add file headers and cleanup structure

* Add 'px' to font-size in test outputs

* Remove extra newlines

* Keep track of 'elementChildren' so children are positioned in the correct order

* Use a ViewVisitor to pass the correct View type to the proposeSize function

* Add support for view modifiers

* Add frame modifier to demonstrate modifiers

* Fix TestRenderer

* Remove unused property

* Fix doc comment

* Fix linter issues and refactor slightly

* Fix benchmark builds

* Attempt to fix benchmarks

* Fix sibling layout issues

* Restore original demo

* Support overriding visit function in renderer and _ShapeView drawing

* Support background modifier

* Resolve reconciler issues due to Optionals and elementIndex being set at wrong phase

* Remove Brewfile.lock.json

* Attempt to fix rendering tests

* Formatting nits

* Fix Gradient rendering

Co-authored-by: Max Desiatov <max@desiatov.com>
2022-06-15 15:33:31 +01:00
Carson Katri 6e2ccf71ea
Add configuration options to `App` to choose reconciler (#495)
* Add support for App/Scene

* Refine reconciler configuration options

* Fix tests

* Fix benchmark

* Address review comments

* Rename shouldLayout to useDynamicLayout in remaining file

* Add note to README about switching reconcilers

* Fix typo

* Add App as a type of Fiber content

* Refactor to avoid temporary variable

* Address review comments

* Add [weak self] to createAndBindAlternate closures

* Remove commented lines
2022-06-05 19:24:05 -04:00
Max Desiatov 9db23c9e3f
Tweak formatting rules for attributes (#492)
The abundance of `@_spi(TokamakCore)` makes it harder to parse some of our code visually when skimming. I propose consistently moving attributes on declarations to separate lines. Here's an update to `.swiftformat` config with the new settings applied to the codebase.

* Tweak formatting rules

* Improve readability with newlines

* More newlines to visually separate declarations

* Fix build error caused by merge conflict

Co-authored-by: Carson Katri <Carson.katri@gmail.com>
2022-05-31 08:18:53 +01:00
Carson Katri f4cd4955db
Add support for Fiber label (#493) 2022-05-30 20:15:26 +00:00
Carson Katri 03513dd5b3
Custom Layout Engine for Fiber Reconciler (#472)
* Initial Reconciler using visitor pattern

* Preliminary static HTML renderer using the new reconciler

* Add environment

* Initial DOM renderer

* Nearly-working and simplified reconciler

* Working reconciler for HTML/DOM renderers

* Rename files, and split code across files

* Add some documentation and refinements

* Remove GraphRendererTests

* Initial layout engine (only implemented for the TestRenderer)

* Layout engine for the DOM renderer

* Refined layout pass

* Revise positioning and restoration of position styles on .update

* Re-add Optional.body for StackReconciler-based renderers

* Add text measurement

* Add spacing to StackLayout

* Add benchmarks to compare the stack/fiber reconcilers

* Fix some issues created for the StackReconciler, and add update benchmarks

* Add BenchmarkState.measure to only calculate the time to update

* Fix hang in update shallow benchmark

* Fix build errors

* Address build issues

* Remove File.swift headers

* Rename Element -> FiberElement and Element.Data -> FiberElement.Content

* Add doc comment explaining unowned usage

* Add doc comments explaining implicitly unwrapped optionals

* Attempt to use Swift instead of JS for applying mutations

* Fix issue with not applying updates to DOMFiberElement

* Add comment explaining manual implementation of Hashable for PropertyInfo

* Fix linter issues

* Remove dynamicMember label from subscript

* Re-enable carton test

* Attempt GTK fix

* Add option to disable layout in the FiberReconciler

* Re-enable TokamakDemo with StackReconciler

* Restore CI config

* Restore CI config

* Add file headers and cleanup structure

* Add 'px' to font-size in test outputs

* Remove extra newlines

* Keep track of 'elementChildren' so children are positioned in the correct order

* Use a ViewVisitor to pass the correct View type to the proposeSize function

* Add support for view modifiers

* Add frame modifier to demonstrate modifiers

* Fix TestRenderer

* Remove unused property

* Fix doc comment

* Fix linter issues and refactor slightly

* Fix benchmark builds

* Attempt to fix benchmarks

* Fix sibling layout issues

* Restore original demo

* Address review comments

* Remove maxAxis and fitAxis properties

* Use switch instead of ternary operators

* Add more documentation to layout steps

* Resolve reconciler issue due to alternate child not being cleared/released

* Apply suggestions from code review

Co-authored-by: Max Desiatov <max@desiatov.com>

* Reuse Text resolution code.

* Add more documentation

* Fix typo

* Use structs for LayoutComputers

* Update AlignmentID demo

* Fix weird formatting

Co-authored-by: Max Desiatov <max@desiatov.com>
2022-05-30 15:49:26 -04:00
Carson Katri 355c880a1d
Support `foregroundColor` for `TextField` (#453)
* Support adjusting TextField foregroundColor in TokamakDOM

* Simplify implementation of foregroundColor
2022-05-28 14:46:37 -04:00
Max Desiatov c0c4534352
Clarify testing commands in `CONTRIBUTING.md` (#488)
This should help new contributors to run the test suite locally.
2022-05-24 15:42:31 +01:00
Max Desiatov a604ef5269
Revert "Add html5 doctype to static renderer (#486)" (#487)
* Revert "Add html5 doctype to static renderer (#486)"

This reverts commit 3081f5521a which causes failing layout snapshots we've recently turned back on in https://github.com/TokamakUI/Tokamak/pull/484.

* Fix newlines inconsistency
2022-05-24 10:59:21 +01:00
Andrew Barba 3081f5521a
Add html5 doctype to static renderer (#486)
- Add `<!DOCTYPE html>` to the top of static rendered pages
2022-05-24 10:08:08 +01:00
ezraberch 2e7f561276
Reenable macOS CI builds, using macOS 12 and Xcode 13.4 (#484) 2022-05-24 05:07:22 -04:00
Andrew Barba 2ba548810c
Support meta tags in StaticHTMLRenderer (#483)
This PR adds the ability to control `<head>` tags using a new `HTMLTitle` view and `HTMLMeta` view. Taking inspiration from Next.js `<NextHead>`, you can use these views anywhere in your view hierarchy and they will be hoisted to the top `<head>` section of the html.

Use as a view:

```swift
var body: some View {
  VStack {
    ...
    HTMLTitle("Hello, Tokamak")
    HTMLMeta(charset: "utf-8")
    ...
  }
}
```

Use as a view modifier:

```swift
var body: some View {
  VStack {
    ...
  }
  .htmlTitle("Hello, Tokamak")
  .htmlMeta(charset: "utf-8")
}
```

And the resulting html (no matter where these are used in your view hierarchy):

```html
<html>
  <head>
    <title>Hello, Tokamak</title>
    <meta charset="utf-8">
  </head>
  <body>
    ...
  </body>
</html>
```
2022-05-23 20:03:28 +00:00
Carson Katri 8177fc8cae
Experimental "React Fiber"-like Reconciler (#471)
* Initial Reconciler using visitor pattern

* Preliminary static HTML renderer using the new reconciler

* Add environment

* Initial DOM renderer

* Nearly-working and simplified reconciler

* Working reconciler for HTML/DOM renderers

* Rename files, and split code across files

* Add some documentation and refinements

* Remove GraphRendererTests

* Re-add Optional.body for StackReconciler-based renderers

* Add benchmarks to compare the stack/fiber reconcilers

* Fix some issues created for the StackReconciler, and add update benchmarks

* Add BenchmarkState.measure to only calculate the time to update

* Fix hang in update shallow benchmark

* Fix build errors

* Address build issues

* Remove File.swift headers

* Rename Element -> FiberElement and Element.Data -> FiberElement.Content

* Add doc comment explaining unowned usage

* Add doc comments explaining implicitly unwrapped optionals

* Attempt to use Swift instead of JS for applying mutations

* Fix issue with not applying updates to DOMFiberElement

* Add comment explaining manual implementation of Hashable for PropertyInfo

* Fix linter issues

* Remove dynamicMember label from subscript

* Re-enable carton test

* Re-enable TokamakDemo with StackReconciler

Co-authored-by: Max Desiatov <max@desiatov.com>
2022-05-23 12:14:16 -04:00
Max Desiatov a41ac37500
Update `carton` version in "Requirements" section 2022-05-20 14:05:00 +01:00
208 changed files with 11069 additions and 583 deletions

View File

@ -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.

View File

@ -14,76 +14,88 @@ jobs:
- uses: swiftwasm/swiftwasm-action@v5.6
with:
shell-action: carton bundle --product TokamakDemo
- name: Check binary size
shell: bash
run: |
ls -la Bundle
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
swiftwasm_test_5_6:
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
shell-action: carton test --environment node
# Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
# core_macos_build:
# runs-on: macos-11
core_macos_build:
runs-on: macos-12
# steps:
# - uses: actions/checkout@v2
# - name: Run the test suite on macOS, build the demo project for iOS
# shell: bash
# run: |
# set -ex
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
# # avoid building unrelated products for testing by specifying the test product explicitly
# swift build --product TokamakPackageTests
# `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
# (cp -r /var/folders/*/*/*/*Tests . ; exit 1)
steps:
- uses: actions/checkout@v2
- name: Run the test suite on macOS, build the demo project for iOS
shell: bash
run: |
set -ex
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
# avoid building unrelated products for testing by specifying the test product explicitly
swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
# rm -rf Sources/TokamakGTKCHelpers/*.c
rm -rf Sources/TokamakGTKCHelpers/*.c
# xcodebuild -version
xcodebuild -version
# # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
# # Disable macOS builds until Monterey is available on GHA.
# # xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
# # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
# # xcpretty --color
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
xcpretty --color
# cd "NativeDemo"
# xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
# CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
# xcpretty --color
# cd ..
cd "NativeDemo"
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
xcpretty --color
cd ..
# ./benchmark.sh
./benchmark.sh
# - name: Upload failed snapshots
# uses: actions/upload-artifact@v2
# if: ${{ failure() }}
# with:
# name: Failed snapshots
# path: '*Tests'
- name: Upload failed snapshots
uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: Failed snapshots
path: '*Tests'
# gtk_macos_build:
# runs-on: macos-11
# steps:
# - uses: actions/checkout@v2
# - name: Build the GTK renderer on macOS
# shell: bash
# run: |
# set -ex
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
# brew install gtk+3
# make build
# FIXME: disabled due to build errors, to be investigated
# gtk_macos_build:
# runs-on: macos-12
#
# steps:
# - uses: actions/checkout@v2
# - name: Build the GTK renderer on macOS
# shell: bash
# run: |
# set -ex
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
#
# brew install gtk+3
#
# make build
gtk_ubuntu_18_04_build:
runs-on: ubuntu-latest
container:
image: swiftlang/swift:nightly-bionic
image: swiftlang/swift:nightly-5.7-bionic
steps:
- uses: actions/checkout@v2
@ -98,7 +110,7 @@ jobs:
gtk_ubuntu_20_04_build:
runs-on: ubuntu-latest
container:
image: swiftlang/swift:nightly-focal
image: swiftlang/swift:nightly-5.7-focal
steps:
- uses: actions/checkout@v2

View File

@ -8,26 +8,26 @@ on:
jobs:
codecov:
container:
image: swiftlang/swift:nightly-focal
image: swiftlang/swift:nightly-5.7-focal
runs-on: ubuntu-latest
steps:
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
- name: Checkout Branch
uses: actions/checkout@v2
- name: Build Test Target
run: swift build -Xswiftc -profile-coverage-mapping -Xswiftc -profile-generate --product TokamakPackageTests
- name: Run Tests
run: swift test --enable-code-coverage --skip-build
- name: Generate Branch Coverage Report
uses: mattpolzin/swift-codecov-action@0.7.1
id: cov
with:
MINIMUM_COVERAGE: 15
- name: Post Positive Results
if: ${{ success() }}
run: |
echo "::warning file=Package.swift,line=1,col=1::The current code coverage percentage is passing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
- name: Post Negative Results
if: ${{ failure() }}
run: |
echo "::error file=Package.swift,line=1,col=1::The current code coverage percentage is failing with ${{ steps.cov.outputs.codecov }} (minimum allowed: ${{ steps.cov.outputs.minimum_coverage }}%)."
- 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 }}%)."

View File

@ -24,6 +24,7 @@ jobs:
dependencies,
documentation,
enhancement,
Fiber,
refactor,
SwiftUI compatibility,
test suite,

View File

@ -8,5 +8,7 @@
--maxwidth 100
--wraparguments before-first
--funcattributes prev-line
--typeattributes prev-line
--varattributes prev-line
--disable andOperator
--swiftversion 5.4
--swiftversion 5.6

View File

@ -29,6 +29,19 @@ only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conj
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
`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

View File

@ -46,23 +46,23 @@ let package = Package(
],
dependencies: [
.package(
url: "https://github.com/swiftwasm/JavaScriptKit.git",
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
from: "0.15.0"
),
.package(
url: "https://github.com/OpenCombine/OpenCombine.git",
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
from: "0.12.0"
),
.package(
url: "https://github.com/swiftwasm/OpenCombineJS.git",
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
from: "0.2.0"
),
.package(
url: "https://github.com/google/swift-benchmark",
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
from: "0.1.2"
),
.package(
url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
from: "1.9.0"
),
],
@ -135,6 +135,7 @@ let package = Package(
dependencies: [
.product(name: "Benchmark", package: "swift-benchmark"),
"TokamakCore",
"TokamakTestRenderer",
]
),
.executableTarget(
@ -194,6 +195,25 @@ 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"]

View File

@ -138,6 +138,31 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
localized date formatting (or any arbitrary style/script/font added that way) are available in your
app.
### Fiber renderers
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
is optionally available. It can provide faster updates and allow for larger View hierarchies.
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
You can specify which reconciler to use in your `App`'s configuration:
```swift
struct CounterApp: App {
static let _configuration: _AppConfiguration = .init(
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
reconciler: .fiber(useDynamicLayout: true)
)
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
```
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
## Requirements
### For app developers
@ -147,7 +172,7 @@ app.
and macOS at the same time.
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
Other Linux distributions are currently not supported.
- [`carton` 0.14.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
### For users of apps depending on Tokamak
@ -216,6 +241,9 @@ 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
@ -297,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), [Ezra Berch](https://github.com/ezraberch),
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
[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

View File

@ -40,14 +40,20 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
}
}
@frozen public struct EmptyAnimatableData: VectorArithmetic {
@frozen
public struct EmptyAnimatableData: VectorArithmetic {
@inlinable
public init() {}
@inlinable public static var zero: Self { .init() }
@inlinable
public static var zero: Self { .init() }
@inlinable
public static func += (lhs: inout Self, rhs: Self) {}
@inlinable
public static func -= (lhs: inout Self, rhs: Self) {}
@inlinable
public static func + (lhs: Self, rhs: Self) -> Self {
.zero
@ -60,11 +66,15 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
@inlinable
public mutating func scale(by rhs: Double) {}
@inlinable public var magnitudeSquared: Double { .zero }
@inlinable
public var magnitudeSquared: Double { .zero }
public static func == (a: Self, b: Self) -> Bool { true }
}
@frozen public struct AnimatablePair<First, Second>: VectorArithmetic
@frozen
public struct AnimatablePair<First, Second>: VectorArithmetic
where First: VectorArithmetic, Second: VectorArithmetic
{
public var first: First
@ -81,7 +91,8 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
set { (first, second) = newValue }
}
@_transparent public static var zero: Self {
@_transparent
public static var zero: Self {
@_transparent get {
.init(First.zero, Second.zero)
}
@ -115,7 +126,8 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
second.scale(by: rhs)
}
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get {
first.magnitudeSquared + second.magnitudeSquared
}

View File

@ -139,7 +139,8 @@ public struct _AnimationProxy {
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
}
@frozen public struct _AnimationModifier<Value>: ViewModifier, Equatable
@frozen
public struct _AnimationModifier<Value>: ViewModifier, Equatable
where Value: Equatable
{
public var animation: Animation?
@ -155,7 +156,9 @@ public struct _AnimationProxy {
let content: Content
let animation: Animation?
let value: Value
@State private var lastValue: Value?
@State
private var lastValue: Value?
var body: some View {
content.transaction {
@ -180,7 +183,8 @@ public struct _AnimationProxy {
}
}
@frozen public struct _AnimationView<Content>: View
@frozen
public struct _AnimationView<Content>: View
where Content: Equatable, Content: View
{
public var content: Content

View File

@ -13,7 +13,7 @@
// limitations under the License.
public struct Transaction {
/// The overriden transaction for a state change in a `withTransaction` block.
/// The overridden transaction for a state change in a `withTransaction` block.
/// Is always set back to `nil` when the block exits.
static var _active: Self?
@ -50,7 +50,8 @@ protocol _TransactionModifierProtocol {
func modifyTransaction(_ transaction: inout Transaction)
}
@frozen public struct _TransactionModifier: ViewModifier {
@frozen
public struct _TransactionModifier: ViewModifier {
public var transform: (inout Transaction) -> ()
@inlinable
@ -77,7 +78,8 @@ extension ModifiedContent: _TransactionModifierProtocol
}
}
@frozen public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
@frozen
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
public var content: V
public var base: _TransactionModifier

View File

@ -25,7 +25,9 @@ public protocol VectorArithmetic: AdditiveArithmetic {
extension Float: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) }
}
}
@ -33,7 +35,9 @@ extension Float: VectorArithmetic {
extension Double: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= rhs }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { self * self }
}
}
@ -41,7 +45,9 @@ extension Double: VectorArithmetic {
extension CGFloat: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
@_transparent public var magnitudeSquared: Double {
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) }
}
}

View File

@ -21,7 +21,7 @@ import Foundation
public protocol _AnimationSolver {
/// Solve value at a specific point in time.
func solve(at t: Double) -> Double
/// Calculates the duration of the animation to a specific presision.
/// Calculates the duration of the animation to a specific precision.
func restingPoint(precision y: Double) -> Double
}

View File

@ -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 ?? ()
}
}

View File

@ -20,7 +20,8 @@ import Foundation
public protocol _VectorMath: Animatable {}
public extension _VectorMath {
@inlinable var magnitude: Double {
@inlinable
var magnitude: Double {
animatableData.magnitudeSquared.squareRoot()
}

View File

@ -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)
}
}

View File

@ -17,9 +17,13 @@
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!
}

View File

@ -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 }
}

View File

@ -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)
}
)
}
}

View File

@ -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) -> ()

View File

@ -75,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)
}
}

View File

@ -42,7 +42,7 @@ public struct _AnyApp: App {
}
@_spi(TokamakCore)
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
public static func _launch(_ app: Self, with configuration: _AppConfiguration) {
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
}
@ -51,6 +51,10 @@ public struct _AnyApp: App {
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
}
public static var _configuration: _AppConfiguration {
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
}
@_spi(TokamakCore)
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")

View File

@ -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 {

View File

@ -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)

View File

@ -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
}
}

View File

@ -17,10 +17,12 @@
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>

View File

@ -76,7 +76,7 @@ public extension EnvironmentValues {
}
}
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
let environmentValues: EnvironmentValues
func body(content: Content) -> some View {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
)
}
}

View File

@ -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) ?? "")
"""
}
}

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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 }
}
}

View File

@ -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 }
}
}

View File

@ -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 }
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
})
}
}

View File

@ -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)
}
}

View File

@ -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) }
}

View File

@ -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))
}
}
}

View File

@ -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
}
)
}
}

View File

@ -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
})
}
}

View File

@ -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
})
}
}

View File

@ -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)
}

View File

@ -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
)
}

View File

@ -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() }
}

View File

@ -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() }
}

View File

@ -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)
}
}

View File

@ -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>
}

View File

@ -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))
}
}

View File

@ -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)]
}
}

View File

@ -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>
}

View File

@ -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!
}
}

View File

@ -14,7 +14,8 @@
import Foundation
@frozen public enum ContentMode: Hashable, CaseIterable {
@frozen
public enum ContentMode: Hashable, CaseIterable {
case fit
case fill
}

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct _OffsetEffect: GeometryEffect, Equatable {
@frozen
public struct _OffsetEffect: GeometryEffect, Equatable {
public var offset: CGSize
@inlinable

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct _ScaleEffect: GeometryEffect, Equatable {
@frozen
public struct _ScaleEffect: GeometryEffect, Equatable {
public var scale: CGSize
public var anchor: UnitPoint

View File

@ -13,14 +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>: ModifiedContentProtocol {
@Environment(\.self) public var environment
@Environment(\.self)
public var environment
public typealias Body = Never
public private(set) var content: Content
public private(set) var modifier: Modifier
@ -32,7 +34,7 @@ public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
}
extension ModifiedContent: ModifierContainer {
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
}
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {

View File

@ -24,6 +24,18 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
public let content: Content
public let background: Background
public let alignment: Alignment
@_spi(TokamakCore)
public init(content: Content, background: Background, alignment: Alignment) {
self.content = content
self.background = background
self.alignment = alignment
}
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(background)
visitor.visit(content)
}
}
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
@ -77,7 +89,8 @@ public extension View {
}
}
@frozen public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
@frozen
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
where Style: ShapeStyle, Bounds: Shape
{
public var environment: EnvironmentValues!
@ -129,6 +142,11 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
public let content: Content
public let overlay: Overlay
public let alignment: Alignment
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(content)
visitor.visit(overlay)
}
}
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader

View File

@ -16,6 +16,23 @@ public protocol ViewModifier {
typealias Content = _ViewModifier_Content<Self>
associatedtype Body: View
func body(content: Content) -> Self.Body
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor
}
public extension ViewModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(inputs: inputs)
}
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
if Body.self == Never.self {
content.visitChildren(visitor)
} else {
visitor.visit(body(content: content))
}
}
}
public struct _ViewModifier_Content<Modifier>: View
@ -23,15 +40,27 @@ public struct _ViewModifier_Content<Modifier>: View
{
public let modifier: Modifier
public let view: AnyView
let visitChildren: (ViewVisitor) -> ()
public init(modifier: Modifier, view: AnyView) {
self.modifier = modifier
self.view = view
visitChildren = { $0.visit(view) }
}
public init<V: View>(modifier: Modifier, view: V) {
self.modifier = modifier
self.view = AnyView(view)
visitChildren = { $0.visit(view) }
}
public var body: some View {
view
}
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitChildren(visitor)
}
}
public extension View {

View File

@ -78,9 +78,6 @@ 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 {

View File

@ -94,13 +94,9 @@ public class MountedElement<R: Renderer> {
public internal(set) var environmentValues: EnvironmentValues
weak var parent: MountedElement<R>?
/// `didSet` on this field propagates the preference changes up the view tree.
var preferenceStore: _PreferenceStore = .init() {
didSet {
parent?.preferenceStore.merge(with: preferenceStore)
}
}
private(set) weak var parent: MountedElement<R>?
var preferenceStore: _PreferenceStore = .init()
public internal(set) var viewTraits: _ViewTraitStore
@ -110,6 +106,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
connectParentPreferenceStore()
}
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
@ -118,6 +115,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
connectParentPreferenceStore()
}
init(
@ -131,6 +129,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
self.viewTraits = viewTraits
updateEnvironment()
connectParentPreferenceStore()
}
func updateEnvironment() {
@ -145,6 +144,10 @@ public class MountedElement<R: Renderer> {
}
}
func connectParentPreferenceStore() {
preferenceStore.parent = parent?.preferenceStore
}
/// You must call `super.prepareForMount` before all other mounting work.
func prepareForMount(with transaction: Transaction) {
// `GroupView`'s don't really mount, so let their children transition if the group can.

View File

@ -25,12 +25,41 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
static var defaultValue: Value { nil }
}
public struct _PreferenceValue<Key> where Key: PreferenceKey {
final class _PreferenceValueStorage: CustomDebugStringConvertible {
/// Every value the `Key` has had.
var valueList: [Key.Value]
var valueList: [Any]
var debugDescription: String {
valueList.debugDescription
}
init<Key: PreferenceKey>(_ key: Key.Type = Key.self) {
valueList = []
}
init(valueList: [Any]) {
self.valueList = valueList
}
func merge(_ other: _PreferenceValueStorage) {
valueList.append(contentsOf: other.valueList)
}
func reset() {
valueList = []
}
}
public struct _PreferenceValue<Key> where Key: PreferenceKey {
var storage: _PreferenceValueStorage
init(storage: _PreferenceValueStorage) {
self.storage = storage
}
/// The latest value.
public var value: Key.Value {
reduce(valueList)
reduce(storage.valueList.compactMap { $0 as? Key.Value })
}
func reduce(_ values: [Key.Value]) -> Key.Value {
@ -48,38 +77,79 @@ public extension _PreferenceValue {
}
}
public struct _PreferenceStore {
public final class _PreferenceStore: CustomDebugStringConvertible {
/// The values of the `_PreferenceStore` on the last update.
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
/// The backing values of the `_PreferenceStore`.
private var values: [String: Any]
private var values: [ObjectIdentifier: _PreferenceValueStorage]
public init(values: [String: Any] = [:]) {
weak var parent: _PreferenceStore?
public var debugDescription: String {
"Preferences (\(ObjectIdentifier(self))): \(values)"
}
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
previousValues = [:]
self.values = values
}
/// Retrieve a late-binding token for `key`, or save the default value if it does not yet exist.
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
where Key: PreferenceKey
{
values[String(reflecting: key)] as? _PreferenceValue<Key>
?? _PreferenceValue(valueList: [Key.defaultValue])
let keyID = ObjectIdentifier(key)
let storage: _PreferenceValueStorage
if let existing = values[keyID] {
storage = existing
} else {
storage = .init(key)
values[keyID] = storage
}
return _PreferenceValue(storage: storage)
}
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
/// Retrieve the value `Key` had on the last update.
///
/// Used to check if the value changed during the last update.
func previousValue<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
where Key: PreferenceKey
{
let previousValues = self.value(forKey: key).valueList
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key))
}
public mutating func merge(with other: Self) {
self = merging(with: other)
}
public func merging(with other: Self) -> Self {
var result = values
for (key, value) in other.values {
result[key] = value
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
where Key: PreferenceKey
{
let keyID = ObjectIdentifier(key)
if !values.keys.contains(keyID) {
values[keyID] = .init(key)
}
values[keyID]?.valueList.append(value)
parent?.insert(value, forKey: key)
}
func merge(_ other: _PreferenceStore) {
for (key, otherStorage) in other.values {
if let storage = values[key] {
storage.merge(otherStorage)
} else {
values[key] = .init(valueList: otherStorage.valueList)
}
}
}
/// Copies `values` to `previousValues`, and clears `values`.
///
/// Each reconcile pass the preferences are collected from scratch, so we need to
/// clear out the old values.
func reset() {
previousValues = values.mapValues {
_PreferenceValueStorage(valueList: $0.valueList)
}
for storage in values.values {
storage.reset()
}
return .init(values: result)
}
}

View File

@ -25,12 +25,26 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
let value = preferenceStore.value(forKey: Key.self)
let previousValue = value.reduce(value.valueList.dropLast())
let previousValue = value.reduce((value.storage.valueList as? [Key.Value] ?? []).dropLast())
if previousValue != value.value {
action(value.value)
}
return content.view
}
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: {
let value = $0.value(forKey: Key.self).value
let previousValue = $0.previousValue(forKey: Key.self).value
if value != previousValue {
inputs.content.action(value)
}
}
)
}
}
public extension View {

View File

@ -20,11 +20,12 @@
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
where Key: PreferenceKey, Content: View
{
@State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
valueList: [Key.defaultValue]
)
@State
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self))
public let transform: (_PreferenceValue<Key>) -> Content
private var valueReference: _PreferenceValue<Key>?
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
self.transform = transform
}
@ -34,7 +35,18 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
}
public var body: some View {
transform(resolvedValue)
transform(valueReference ?? resolvedValue)
}
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
let preferenceStore = inputs.preferenceStore ?? .init()
inputs.updateContent {
$0.valueReference = preferenceStore.value(forKey: Key.self)
}
return .init(
inputs: inputs,
preferenceStore: preferenceStore
)
}
}
@ -69,7 +81,7 @@ public extension View {
) -> some View
where Key: PreferenceKey, T: View
{
Key._delay { self.overlay(transform($0.value)) }
Key._delay { self.overlay($0._force(transform)) }
}
func backgroundPreferenceValue<Key, T>(
@ -78,6 +90,6 @@ public extension View {
) -> some View
where Key: PreferenceKey, T: View
{
Key._delay { self.background(transform($0.value)) }
Key._delay { self.background($0._force(transform)) }
}
}

View File

@ -34,6 +34,18 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
preferenceStore.insert(newValue, forKey: Key.self)
return content.view
}
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: {
var value = $0.value(forKey: Key.self).value
inputs.content.transform(&value)
$0.insert(value, forKey: Key.self)
}
)
}
}
public extension View {

View File

@ -27,6 +27,14 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
preferenceStore.insert(value, forKey: Key.self)
return content.view
}
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
.init(
inputs: inputs,
preferenceStore: inputs.preferenceStore ?? .init(),
preferenceAction: { $0.insert(inputs.content.value, forKey: Key.self) }
)
}
}
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {

View File

@ -18,7 +18,8 @@ import Foundation
public protocol PreviewProvider {
associatedtype Previews: View
@ViewBuilder static var previews: Previews { get }
@ViewBuilder
static var previews: Previews { get }
}
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {

View File

@ -20,7 +20,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
typealias FieldTypeAccessor = @convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
typealias FieldTypeAccessor = @convention(c)
(UnsafePointer<Int>) -> UnsafePointer<Int>
struct StructTypeDescriptor {
let flags: Int32

View File

@ -20,7 +20,21 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
public struct PropertyInfo {
public struct PropertyInfo: Hashable {
// Hashable/Equatable conformance is not synthesize for metatypes.
public static func == (lhs: PropertyInfo, rhs: PropertyInfo) -> Bool {
lhs.name == rhs.name && lhs.type == rhs.type && lhs.isVar == rhs.isVar && lhs.offset == rhs
.offset && lhs.ownerType == rhs.ownerType
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(ObjectIdentifier(type))
hasher.combine(isVar)
hasher.combine(offset)
hasher.combine(ObjectIdentifier(ownerType))
}
public let name: String
public let type: Any.Type
public let isVar: Bool

View File

@ -24,7 +24,9 @@ struct MetadataOffset<Pointee> {
let offset: Int32
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
#if arch(wasm32)
// Data pointers in constant metadata are absolute until SwiftWasm 5.6
// Since SwiftWasm 5.7, they are relative as well as other platforms.
#if arch(wasm32) && !compiler(>=5.7)
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
#else
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)

View File

@ -0,0 +1,76 @@
// Copyright 2022 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
protocol AnyShapeBox {
var animatableDataBox: _AnyAnimatableData { get set }
func path(in rect: CGRect) -> Path
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}
private struct ConcreteAnyShapeBox<Base: Shape>: AnyShapeBox {
var base: Base
var animatableDataBox: _AnyAnimatableData {
get {
_AnyAnimatableData(base.animatableData)
}
set {
guard let newData = newValue.value as? Base.AnimatableData else {
// TODO: Should this crash?
return
}
base.animatableData = newData
}
}
func path(in rect: CGRect) -> Path {
base.path(in: rect)
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
base.sizeThatFits(proposal)
}
}
public struct AnyShape: Shape {
var box: AnyShapeBox
private init(_ box: AnyShapeBox) {
self.box = box
}
}
public extension AnyShape {
init<S: Shape>(_ shape: S) {
box = ConcreteAnyShapeBox(base: shape)
}
func path(in rect: CGRect) -> Path {
box.path(in: rect)
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
box.sizeThatFits(proposal)
}
var animatableData: _AnyAnimatableData {
get { box.animatableDataBox }
set { box.animatableDataBox = newValue }
}
}

View File

@ -38,7 +38,8 @@ extension ContainerRelativeShape: InsettableShape {
}
@usableFromInline
@frozen internal struct _Inset: InsettableShape, DynamicProperty {
@frozen
internal struct _Inset: InsettableShape, DynamicProperty {
@usableFromInline
internal var amount: CGFloat
@inlinable
@ -76,7 +77,8 @@ private extension EnvironmentValues {
}
}
@frozen public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
@frozen
public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
public var shape: Shape
@inlinable
public init(shape: Shape) { self.shape = shape }

View File

@ -18,7 +18,9 @@
import Foundation
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
@Environment(\.self) public var environment
@Environment(\.self)
public var environment
public var shape: S
public var style: StrokeStyle

View File

@ -89,8 +89,11 @@ extension RoundedRectangle: InsettableShape {
@usableFromInline
struct _Inset: InsettableShape {
@usableFromInline var base: RoundedRectangle
@usableFromInline var amount: CGFloat
@usableFromInline
var base: RoundedRectangle
@usableFromInline
var amount: CGFloat
@inlinable
init(base: RoundedRectangle, amount: CGFloat) {

View File

@ -21,6 +21,18 @@ public protocol Shape: Animatable, View {
func path(in rect: CGRect) -> Path
static var role: ShapeRole { get }
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}
public extension Shape {
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
// TODO: Check if SwiftUI changes this behavior.
// SwiftUI seems to not compute the path at all and just return
// the following.
proposal.replacingUnspecifiedDimensions()
}
}
public enum ShapeRole: Hashable {
@ -54,9 +66,15 @@ public struct FillStyle: Equatable {
}
}
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
@Environment(\.self) public var environment
@Environment(\.foregroundColor) public var foregroundColor
public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content: Shape,
Style: ShapeStyle
{
@Environment(\.self)
public var environment
@Environment(\.foregroundColor)
public var foregroundColor
public var shape: Content
public var style: Style
public var fillStyle: FillStyle
@ -66,6 +84,32 @@ public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, S
self.style = style
self.fillStyle = fillStyle
}
public func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
.init()
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
for subview in subviews {
subview.place(
at: bounds.origin,
proposal: .init(width: bounds.width, height: bounds.height)
)
}
}
}
public extension Shape {

View File

@ -56,7 +56,8 @@ public extension View {
}
}
@frozen public struct _BackgroundStyleModifier<Style>: ViewModifier, EnvironmentModifier,
@frozen
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
EnvironmentReader
where Style: ShapeStyle
{

View File

@ -71,9 +71,10 @@ public extension View {
}
}
@frozen public struct _ForegroundStyleModifier<
@frozen
public struct _ForegroundStyleModifier<
Primary, Secondary, Tertiary
>: ViewModifier, EnvironmentModifier
>: ViewModifier, _EnvironmentModifier
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
{
public var primary: Primary

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct AngularGradient: ShapeStyle, View {
@frozen
public struct AngularGradient: ShapeStyle, View {
internal var gradient: Gradient
internal var center: UnitPoint
internal var startAngle: Angle

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct EllipticalGradient: ShapeStyle, View {
@frozen
public struct EllipticalGradient: ShapeStyle, View {
internal var gradient: Gradient
internal var center: UnitPoint
internal var startRadiusFraction: CGFloat

View File

@ -17,8 +17,10 @@
import Foundation
@frozen public struct Gradient: Equatable {
@frozen public struct Stop: Equatable {
@frozen
public struct Gradient: Equatable {
@frozen
public struct Stop: Equatable {
public var color: Color
public var location: CGFloat

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct LinearGradient: ShapeStyle, View {
@frozen
public struct LinearGradient: ShapeStyle, View {
internal var gradient: Gradient
internal var startPoint: UnitPoint
internal var endPoint: UnitPoint

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct RadialGradient: ShapeStyle, View {
@frozen
public struct RadialGradient: ShapeStyle, View {
internal var gradient: Gradient
internal var center: UnitPoint
internal var startRadius: CGFloat

View File

@ -16,7 +16,8 @@
//
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
@frozen public struct HierarchicalShapeStyle: ShapeStyle {
@frozen
public struct HierarchicalShapeStyle: ShapeStyle {
@usableFromInline
internal var id: UInt32

View File

@ -17,6 +17,10 @@
import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
#endif
public struct StrokeStyle: Equatable {
public var lineWidth: CGFloat
public var lineCap: CGLineCap

View File

@ -58,6 +58,12 @@ public final class StackReconciler<R: Renderer> {
*/
public let rootTarget: R.TargetType
/** A root renderer's main preference store.
*/
public var preferenceStore: _PreferenceStore {
rootElement.preferenceStore
}
/** A root of the mounted elements tree to which all other mounted elements are attached to.
*/
private let rootElement: MountedElement<R>

View File

@ -23,7 +23,8 @@ protocol WritableValueStorage: ValueStorage {
var setter: ((Any, Transaction) -> ())? { get set }
}
@propertyWrapper public struct State<Value>: DynamicProperty {
@propertyWrapper
public struct State<Value>: DynamicProperty {
private let initialValue: Value
var anyInitialValue: Any { initialValue }

View File

@ -103,7 +103,7 @@ public struct CGAffineTransform: Equatable {
/// Returns an affine transformation matrix constructed from a rotation value you provide.
/// - Parameters:
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
/// A positive value specifies clockwise rotation and anegative value specifies
/// A positive value specifies clockwise rotation and a negative value specifies
/// counterclockwise rotation.
public init(rotationAngle angle: CGFloat) {
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)

View File

@ -25,7 +25,8 @@
public struct ToggleStyleConfiguration {
public let label: AnyView
@Binding public var isOn: Swift.Bool
@Binding
public var isOn: Swift.Bool
}
public protocol ToggleStyle {

View File

@ -0,0 +1,32 @@
// Copyright 2022 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 6/20/22.
//
public enum LayoutDirection: Hashable, CaseIterable {
case leftToRight
case rightToLeft
}
extension EnvironmentValues {
private enum LayoutDirectionKey: EnvironmentKey {
static var defaultValue: LayoutDirection = .leftToRight
}
public var layoutDirection: LayoutDirection {
get { self[LayoutDirectionKey.self] }
set { self[LayoutDirectionKey.self] = newValue }
}
}

View File

@ -37,11 +37,13 @@ struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
case tagged(V)
}
@inlinable static var defaultValue: Value { .untagged }
@inlinable
static var defaultValue: Value { .untagged }
}
@usableFromInline
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
@inlinable static var defaultValue: Bool { false }
@inlinable
static var defaultValue: Bool { false }
@usableFromInline typealias Value = Bool
}

View File

@ -17,7 +17,8 @@
import Foundation
@frozen public struct AnyTransition {
@frozen
public struct AnyTransition {
fileprivate let box: _AnyTransitionBox
private init(_ box: _AnyTransitionBox) {
@ -27,14 +28,16 @@ import Foundation
@usableFromInline
struct TransitionTraitKey: _ViewTraitKey {
@inlinable static var defaultValue: AnyTransition { .opacity }
@inlinable
static var defaultValue: AnyTransition { .opacity }
@usableFromInline typealias Value = AnyTransition
}
@usableFromInline
struct CanTransitionTraitKey: _ViewTraitKey {
@inlinable static var defaultValue: Bool { false }
@inlinable
static var defaultValue: Bool { false }
@usableFromInline typealias Value = Bool
}

View File

@ -24,7 +24,8 @@ public protocol _TraitWritingModifierProtocol {
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
}
@frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
@frozen
public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
where Trait: _ViewTraitKey
{
public let value: Trait.Value
@ -40,6 +41,14 @@ public protocol _TraitWritingModifierProtocol {
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
viewTraitStore.insert(value, forKey: Trait.self)
}
public static func _makeView(_ inputs: ViewInputs<_TraitWritingModifier<Trait>>)
-> ViewOutputs
{
var store = inputs.traits ?? .init()
store.insert(inputs.content.value, forKey: Trait.self)
return .init(inputs: inputs, traits: store)
}
}
extension ModifiedContent: _TraitWritingModifierProtocol

View File

@ -16,21 +16,21 @@
//
public struct _ViewTraitStore {
public var values = [String: Any]()
public var values = [ObjectIdentifier: Any]()
public init(values: [String: Any] = [:]) {
public init(values: [ObjectIdentifier: Any] = [:]) {
self.values = values
}
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
where Key: _ViewTraitKey
{
values[String(reflecting: key)] as? Key.Value ?? Key.defaultValue
values[ObjectIdentifier(key)] as? Key.Value ?? Key.defaultValue
}
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
where Key: _ViewTraitKey
{
values[String(reflecting: key)] = value
values[ObjectIdentifier(key)] = value
}
}

View File

@ -40,6 +40,8 @@ public struct AnyView: _PrimitiveView {
*/
let bodyType: Any.Type
let visitChildren: (ViewVisitor, Any) -> ()
public init<V>(_ view: V) where V: View {
if let anyView = view as? AnyView {
self = anyView
@ -52,8 +54,14 @@ public struct AnyView: _PrimitiveView {
self.view = view
// swiftlint:disable:next force_cast
bodyClosure = { AnyView(($0 as! V).body) }
// swiftlint:disable:next force_cast
visitChildren = { $0.visit($1 as! V) }
}
}
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitChildren(visitor, view)
}
}
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {

Some files were not shown because too many files have changed in this diff Show More