Compare commits

...

103 Commits

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
Max Desiatov 5b7e7058e8 Bump version to 0.10.1, update `CHANGELOG.md` 2022-05-20 13:48:32 +01:00
Max Desiatov 337b80a4c4
Update JSKit dependency (#482)
* Update JSKit dependency

* Fix deprecations in `Package.swift`

* Use `main` branch of JSKit

* Update JavaScriptKit dependency to stable version
2022-05-19 14:53:48 +01:00
Max Desiatov 528fe056e0
Explicitly mention `carton` version in "Requirements" (#481)
* Explicitly mention `carton` version in "Requirements"

This makes the version number of `carton` required for successful builds more explicit.

* Update README.md

Co-authored-by: Jed Fox <git@jedfox.com>
2022-05-10 15:41:05 +01:00
Max Desiatov 39d37a96c8
Use stable `v5.6` version of `swiftwasm-action` (#477)
After `carton` 0.14.0 was tagged, I've updated `swiftwasm-action` to use that latest release. With that, corresponding `v5.6` was tagged, which I propose to use in `ci.yml` instead of the `main` branch.
2022-05-02 22:07:17 +01:00
Max Desiatov 6905fdff19
Bump version to 0.10.0, update `CHANGELOG.md` (#476)
I'd be happy to include #471 as well, but I personally prefer rolling out big changes in stages. #471 deserves its own release, while in this release the only meaningful change is the Swift version requirement and JSKit/`carton` dependencies bump.
2022-04-09 12:55:30 +01:00
Max Desiatov fd64eafde8
Build and test with SwiftWasm 5.6 on CI (#475)
Had to drop support for Swift 5.4/5.5 and macOS 5.6 jobs, see https://github.com/TokamakUI/Tokamak/pull/475#issuecomment-1092662828 for more details.

Linux builds and `codecov` job were updated to use nightly Swift, which have crashes reproducible in 5.6.0 release fixed.

Also applied a few formatting changes with the latest SwiftFormat.
2022-04-08 21:36:52 +01:00
Max Desiatov eef6bb2da3
Update "Contributing" section in `README.md` 2022-02-21 15:51:47 +00:00
Max Desiatov 1952170ce8
Update "Contributing" section of `README.md` 2022-02-21 15:50:06 +00:00
Max Desiatov 3c649d5ead
Update CHANGELOG.md 2022-02-16 10:41:23 +00:00
yonihemi 12606a809e
Update JavascriptKit (#468)
* Bump JavaScriptKit dependency to 0.12.0.

* Update CHANGELOG.md for 0.9.1 release.

* Fix access to `hash` property

(now that JSObject conforms to Hashable it has a Swift hash property which overrides callAsFunction)

* Update CHANGELOG.md
2022-02-16 10:41:05 +00:00
Vincent Esche a9addc8cb1
Fix `rootEnvironment` not merged with `.defaultEnvironment` (#461)
* Make actual use of `rootEnvironment` passed into functions, falling back to `.defaultEnvironment`

* Add `.merge(_:)`/`.merging(_:)` to `EnvironmentValues`

* Merge `.defaultEnvironment` with `rootEnvironment`

* Add `@_spi(TokamakCore)` protection for `EnvironmentValues.merge(_:)`/`.merging(_:)`
2022-01-03 08:13:14 -05:00
Vincent Esche 077c0cdfcb
Fix typos (#462) 2021-12-31 18:07:30 +00:00
Max Desiatov 5c3bc9d783
Build and test with SwiftWasm 5.5 on CI (#460)
Configuration for simultaneous builds with SwiftWasm 5.4 and 5.5 can't be specified more succinctly due to https://github.com/swiftwasm/swiftwasm-action/issues/3. I had to create almost duplicate job descriptions because of that.
2021-12-21 12:52:50 +01:00
Max Desiatov d2d79c2bbf
Add `DynamicHTML` example code to `README.md` (#459)
This should help our users to get started with `DynamicHTML`.
2021-12-13 18:00:43 +01:00
Max Desiatov 32616fe9d4
Update `CHANGELOG.md` for 0.9.0 release (#458) 2021-11-26 18:06:05 +01:00
Max Desiatov cbfdc34793
Update for JSKit 0.11.1, add async `task` modifier (#457)
I've also moved sources in `TokamakDemo` directory into their respective subdirectories for easier navigation.

* Update for JSKit 0.11.0, add async `task` modifier

* Add back new file locations to `NativeDemo`

* Add compiler `#if` check to `TaskDemo`

* Update to JavaScriptKit 0.11.1

* Restrict `TaskDemo` with `compiler(>=5.5)` check

* Replace `compiler` with `swift` in some places

* Revert "Replace `compiler` with `swift` in some places"

This reverts commit 534784ca7b.

* Use Xcode 13.2 on GitHub Actions hosts

* Find `TokamakPackageTests` in the build directory

* Fix macOS tests bundle path

* Make `task` modifier available only on macOS Monterey

* Revert "Use Xcode 13.2 on GitHub Actions hosts"

This reverts commit 63d044f2d5.

* Revert "Fix macOS tests bundle path"

This reverts commit 3ccbc98a2d.

* Revert "Find `TokamakPackageTests` in the build directory"

This reverts commit 68c845bc19.

* Use `canImport(Concurrency)` as an ultimate check

* Use `compiler(>=5.5) && canImport(Concurrency)`

* Clarify new browser version requirements in `README.md`

* Account for `_Concurrency` naming

* Update `README.md`

* Update README.md

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>
2021-11-23 15:31:28 +01:00
Max Desiatov a5a05b4826
Mark Xcode 13.1 as unsupported in `README.md` 2021-11-12 12:34:23 +01:00
Max Desiatov 2ad85b329d
Switch to Xcode 13.0 in `gtk_macos_build` job (#454) 2021-10-23 21:26:39 +01:00
Max Desiatov f07b9fa883
Link to `CONTRIBUTING.md` from `README.md` 2021-10-01 12:11:24 +01:00
Max Desiatov dfc79e4148
Create CONTRIBUTING.md 2021-10-01 12:11:00 +01:00
Carson Katri 005996262a
Add Canvas and TimelineView to DOM renderer (#449)
* Add initial implementations of Canvas and TimelineView

* Add CanvasDemo

* Add the demo to the native project

* Use Xcode 13.0 for macOS builds

* Disable macOS builds until Monterey is available

* Mark CanvasDemo as iOS 15/macOS 12 only, fix LinkButtonStyle reference on iOS

* Add _VariadicView and symbol rendering

* Fix linter warnings

* Add image support

* Revise AnimationTimelineSchedule and requestAnimationFrame cancellation

* Fix pausing of animated TimelineView in TokamakDOM

Co-authored-by: Max Desiatov <max@desiatov.com>
2021-09-28 10:27:35 -04:00
Adam Gastineau da66063918
Initial implementation of `onHover` (#448)
Adds a basic implementation of action modifiers, allowing for dynamic event handling. For testing, adds a trivial `onHover` example.
2021-09-20 16:13:30 +01:00
ezraberch 8788fd64e9
Refactor NavigationView (#446)
Workaround for #445
2021-09-16 11:17:30 -04:00
Brandon Williams 88cee68bee
Save HTML snapshots with .html extension. (#447)
This is a small thing I noticed while browsing the code. If you save the snapshots with the `html` extension then you can make it easy to see preview the actual html in a browser locally.

* Save HTML snapshots with .html extension.

* Apply suggestions from code review

Co-authored-by: Max Desiatov <max@desiatov.com>
2021-09-11 20:12:11 +01:00
ezraberch 19bcf2746b
Add HTML renderer support for AngularGradient (#444)
* Add HTML renderer support for AngularGradient
* Change UnitPoint constants to match SwiftUI
2021-09-10 10:45:29 -04:00
Max Desiatov 3d9558f1b8
Bump requirements to Swift 5.4, migrate to `@resultBuilder` (#442)
This updates the project to use Swift 5.4 across all platforms. Swift 5.4 is now also the required version, which allows us to use `@resultBuilder` instead of the deprecated version of this attribute from Swift 5.3.

Use `carton` 0.11.0 or later from now on to build with SwiftWasm 5.4.0.
2021-09-08 10:18:53 +01:00
ezraberch e1fcd180d7
Add HTML sanitizer to Text (#437)
* Add HTML sanitizer to Text
* Add Security section to README
2021-08-24 11:27:45 -04:00
Max Desiatov bd1d8138c3
Add `@ezraberch` to the list of maintainers (#440)
* Add `@ezraberch` to the list of maintainers

* Update README.md

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>
2021-08-17 13:15:07 +01:00
Max Desiatov c2ed28ca40
Update `CHANGELOG.md` for the 0.8.0 release (#438)
* Update `CHANGELOG.md` for the 0.8.0 release

* Update `CHANGELOG.md`

* Apply suggestions from code review

Co-authored-by: Carson Katri <Carson.katri@gmail.com>
2021-08-16 18:13:57 +01:00
Carson Katri 4609b0a203
Revise ShapeStyle and add Gradients (#435) 2021-08-14 18:26:39 -04:00
Carson Katri 21c21cd328
Add `Toolbar` implementation for HTML renderer (#169)
`Toolbar` is new in SwiftUI. It is coupled fairly closely with `NavigationView`, so this should be integrated with that somehow (#130). It was made similar to macOS which allows more than a leading/trailing `ToolbarItem`.

Resolves #316.
2021-08-05 16:17:30 +01:00
ezraberch a8c6eae94e
Fix SwiftLint action (#434)
* Update SwiftLint action
2021-07-28 09:56:25 -04:00
Carson Katri 9a568ab9cf
Add View Traits and transitions (#426) 2021-07-28 09:40:12 -04:00
Max Desiatov ae0db4d1f1
Add `ToolbarItem` and its builder functions (#430)
No functional change is introduced, only core APIs are added but aren't implemented anywhere yet.

This is a step towards reducing the size of #169.

* Add `ToolbarItem` and its builder functions

* Silence file_length linter warning
2021-07-25 16:01:29 +01:00
Carson Katri 22ea230ce0
Add controlSize/controlProminence modifiers (#431) 2021-07-19 13:58:14 -04:00
Carson Katri 6792dbb02c
Fix background/overlay layout in DOM/HTML renderers (#429) 2021-07-17 15:45:32 -04:00
Carson Katri 12a6256ec0
Add ProgressView (#425)
This adds `ProgressView` using the `<progress>` tag on the web.

* Add ProgressView implementation

* Fix native demo

* Enable Foundation.Progress in non-WASI environments

* Fix wasm build

* Update progress coc

* Improve snapshot copy error handling

* Use RenderingTests as directory name

* Fix snapshots CI script

* Make test failures fail the CI job

* Snapshot script debugging

* Copy failed snapshots in a different way

* Call `exit 1` when tests fail

* Use correct directory in the upload step

* Update test image

* Update .github/workflows/ci.yml

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>
Co-authored-by: Max Desiatov <max@desiatov.com>
2021-07-17 16:43:51 +01:00
Carson Katri ab5e564ada
Animation implementation using the Web Animations API (#427) 2021-07-13 08:48:45 -04:00
Carson Katri a064956095
Add `scaleEffect` modifier (#424) 2021-07-13 08:48:28 -04:00
Carson Katri ff3f81dbfd
Add `aspectRatio` modifier (#422) 2021-07-12 16:59:24 -04:00
Carson Katri b6790c5c6d
Add support for custom fonts (#421) 2021-07-12 12:10:26 -04:00
Carson Katri 30f55d9814
Check minWidth/Height == nil (#420) 2021-07-09 09:58:38 -04:00
Carson Katri 2efa80a57d
Add Primary/Secondary/Tertiary/QuaternaryContentStyle (#419) 2021-07-07 18:44:34 -04:00
Carson Katri 4a7748ad6b
Add `Material` to the HTML renderer (#418) 2021-07-07 18:09:13 -04:00
Carson Katri 54146b8a38
Improve ShapeStyles to match iOS 15+ (#417) 2021-07-07 16:04:26 -04:00
Carson Katri 79a9a66da2
Add ContainerRelativeShape (#416) 2021-07-07 13:21:07 -04:00
Max Desiatov 738455be68
Add HTML implementation for `opacity` modifier (#415)
* Support `spacing` property on `HStack`/`VStack`

* Remove unused properties in `StackDemo`

* Implement stack spacing with grid gaps

* Fix GTK build

* Bump browser requirements in README.md

* Remove outdated FIXME

* Generalize snapshot timeouts

* Prevent excessive CSS style leaks of properties

* Add HTML implementation for `opacity` modifier

* Update tests name to reflect file name

* Fix missing semicolon in opacity style attribute

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>

* Remove duplicate declaration

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>
2021-07-07 14:14:14 +01:00
Max Desiatov 719c109811
Support `spacing` property on `HStack`/`VStack` (#273)
It's much easier to implement stack spacing when stacks are rendered as single-row or single-column grids, and grid gaps already work in all browsers. For this we need to slightly bump browser version requirements, most notably from Safari 11 to Safari 12.

Resolves #272.

* Remove unused properties in `StackDemo`

* Implement stack spacing with grid gaps

* Fix GTK build

* Bump browser requirements in README.md

* Remove outdated FIXME

* Generalize snapshot timeouts

* Prevent excessive CSS style leaks of properties
2021-07-07 14:01:31 +01:00
yonihemi 2dcbc67cd3
Explicitly import CoreFoundation (#413)
This allows TokamakDemo to be built with [latest SwiftWasm 5.4 toolchain](https://github.com/swiftwasm/swift/releases/tag/swift-wasm-5.4-SNAPSHOT-2021-06-17-a) in **debug mode** (compiler still crashes when building for release).

BTW this import could be anywhere in the target, couldn't find a file that felt natural to include it in.

* Move import statement to CGStubs.swift
2021-06-30 14:14:59 +01:00
ezraberch 9aa88a1978
Fix handling of stroked shapes (#414)
The PR fixes multiple bugs which prevent stroked shapes from rendering correctly.

1. Allow environment injection into  `_StrokedShape`. This causes stroked shapes to no longer crash (#322).
2. Change `Path.strokedPath` and `Path.trimmedPath` to allow the `sizing` of the base shape to be inherited. Without this, stroked shapes can appear as 0x0 in size, making them invisible.
3. Change `_ShapeView` in the StaticHTML renderer to merge attributes in the `svg` rather than placing the `svg` in a `div`. This allows proper rendering when multiple shapes are in a stack.

Finally, the `Path` demo has been modified to add a stroked circle.
2021-06-28 12:45:23 +01:00
Max Desiatov d35e37c4f5
Add a snapshot test for `Path` SVG layout (#412)
This adds a dependency on the [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library, which allows testing our SVG layout algorithm end-to-end. We use the `--screenshot` flag of Chromium-based browsers (MS Edge in this case) to produce a PNG snapshot of a view rendered with `StaticHTMLRenderer`.

This test works only on macOS for now due to its dependency on `NSImage`, but that should be fine as we'd expect the same SVG output to be rendered in the same way on all platforms.

* Implement snapshot tests with headless MS Edge

* Increase snapshot tests timeout

* Force 1.0 resolution scale for headless Edge

* Avoid complex layouts in the snapshot test

* Exclude dir from target sources, upload failures

* Add a test to verify that fusion works

* Enable fusion of modifiers nested three times

* Filter out empty attributes

* Run snapshot tests only on macOS for now

* Fully exclude snapshot testing on WASI

* Fix `testOptional` snapshot

* Clean up code formatting

* Copy failed snapshots to a readable directory

* Make the copy script more resilient

* Use `--force-color-profile=srgb` Chromium flag

* Re-enable spooky hanger test

* Clean up testSpookyHanger

* Fix linter warnings

* Fix file_length linter warning

* Silence linter warning for `Text.attributes` func

* Split `PathLayout.swift` to appease the linter
2021-06-21 16:45:21 +01:00
Max Desiatov e6c37a4c80
Attempt `padding` modifier fusion to avoid nested `div`s (#253)
This allows fusing nested `.padding` modifiers into a single `div` that sums up padding values from all these modifiers.

Before:

```swift
Text("text").padding(10).padding(20)
```

rendered to this (text styling omitted for brevity):

```html
<div style="padding-top: 20.0px; padding-left: 20.0px; padding-bottom: 20.0px; padding-right: 20.0px;">
  <div style="padding-top: 10.0px; padding-left: 10.0px; padding-bottom: 10.0px; padding-right: 10.0px;">
    <span>text</span>
  </div>
</div>
```

Now it renders as

```html
<div style="padding-top: 30.0px; padding-left: 30.0px; padding-bottom: 30.0px; padding-right: 30.0px;">
  <span>text</span>
</div>
```

I hope this approach could be applied to other modifier combinations where it makes sense (in separate PRs).

* Attempt `padding` modifier fusion

* Fix linter warning

* Add a test to verify that fusion works

* Enable fusion of modifiers nested three times

* Filter out empty attributes

* Run snapshot tests only on macOS for now

* Fully exclude snapshot testing on WASI

* Fix `testOptional` snapshot

* Clean up code formatting
2021-06-21 16:00:28 +01:00
Max Desiatov ae219e947b
Use `CGFloat`, `CGPoint`, `CGRect` from Foundation (#411)
Resolves #404.

This also allows us to write more tests that are source-compatible with SwiftUI.

* Use `CGFloat`, `CGPoint`, `CGRect` from Foundation

* Fix GTK build

* Fix macOS build
2021-06-19 19:29:44 +01:00
Max Desiatov ac69bbc3e5
Add reconciler stress tests for elaborate testing (#381)
Most of the changes are related to the use of OpenCombineShim (available in upstream OpenCombine now) instead of CombineShim. But there is also a new test added during the investigation of #367, where an app is rendered end-to end, which is a good way to expand our test suite I think.

* Use immediate scheduler in TestRenderer

This allows running our test suite on WASI too, which doesn't have Dispatch and also can't wait on XCTest expectations. Previously none of our tests (especially runtime reflection tests) ran on WASI.

* Run `carton test` and `carton bundle` in separate jobs

* Bump year in the `LICENSE` file

* Add reconciler stress tests for elaborate testing

* Move default App implementation to TestRenderer

* Use OpenCombineShim instead of CombineShim
2021-06-15 23:01:45 +01:00
ezraberch 3302a5163c
Fix rendering of spacers after DOMRenderer.update is called (#410)
`DOMRenderer.mount` contains code which can be necessary to properly render spacers. However, `update` can overwrite what this code does, leading to the problem described in #395.

This PR modifies `update` to fix this issue.
2021-06-15 22:59:33 +01:00
Max Desiatov 5926e9f182
Replace `ViewDeferredToRenderer`, fix renderer tests (#408)
This allows writing tests for `TokamakStaticHTML`, `TokamakDOM`, and `TokamakGTK` targets.

The issue was caused by conflicting `ViewDeferredToRenderer` conformances declared in different modules, including the `TokamakTestRenderer` module. 

This works around a general limitation in Swift, which was [discussed at length on Swift Forums previously](https://forums.swift.org/t/an-implementation-model-for-rational-protocol-conformance-behavior/37171). When multiple conflicting conformances to the same protocol (`ViewDeferredToRenderer` in our case) exist in different modules, only one of them is available in a given binary (even a test binary). Also, only of them can be loaded and used. Which one exactly is loaded can't be known at compile-time, which is hard to debug and leads to breaking tests that cover code in different renderers. We had to disable `TokamakStaticHTMLTests` for this reason.

The workaround is to declare two new functions in the `Renderer` protocol:

```swift
public protocol Renderer: AnyObject {
  // ...
  // Functions unrelated to the issue at hand skipped for brevity.

  /** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
   this renderer.
   */
  func body(for view: Any) -> AnyView?

  /** Returns `true` if a given view type is a primitive view that should be deferred to this
   renderer.
   */
  func isPrimitiveView(_ type: Any.Type) -> Bool
}
```

Now each renderer can declare their own protocols for their primitive views, i.e. `HTMLPrimitive`, `DOMPrimitive`, `GTKPrimitive` etc, delegating to them from the implementations of `body(for view:)` and `isPrimitiveView(_:)`. Conformances to these protocols can't conflict across different modules. Also, these protocols can have `internal` visibility, as opposed to `ViewDeferredToRenderer`, which had to be declared as `public` in `TokamakCore` to be visible in renderer modules.
2021-06-07 17:24:02 +01:00
ezraberch da9843d07f
Allow DOMRenderer to render buttons with non-Text labels (#403) (#409)
Currently, `DOMRenderer` can only handle `Button`s where is the label is `Text`. If it is any other `View`, the `Button` is not rendered. This is the cause of #403.

This PR removes this restriction. Additionally, it expands the `ButtonStyle` demo to include `Button`s with complex labels.
2021-06-07 17:17:43 +01:00
Max Desiatov 44280847cf
Sort attributes in HTML nodes when rendering (#346)
This makes attributes order deterministic and allows testing against HTML renderer output, while currently attributes order is random.

Benchmarks results:

```
name                            time            std        iterations
---------------------------------------------------------------------
render Text                         9667.000 ns ±   4.35 %     145213
render App unsorted attributes     51917.000 ns ±   4.23 %      26835
render App sorted attributes       52375.000 ns ±   1.62 %      26612
render List unsorted attributes 34546833.500 ns ±   0.79 %         40
render List sorted attributes   34620000.500 ns ±   0.69 %         40
```

Looks like on average there's ~0.2% difference in performance. I was leaning towards enabling sorting by default, but we're benchmarking here only with short attribute dictionaries, I wonder if the difference could become prominent for elements with more attributes. I kept sorting disabled by default after all, but still configurable.

`var html: String` on `StaticHTMLRenderer` was changed to `func render(shouldSortAttributes: Bool = false) -> String` to allow configuring this directly.

* Sort attributes in HTML nodes when rendering

* Make sorting configurable, add benchmarks

* Disable sorting by default, clean up product name

* Fix build errors
2021-06-06 18:52:15 +01:00
ezraberch 77759777f1
Fix DOMRenderer crash after DOM has been directly modified. (#326, #369) (#407)
When _domRef is used to directly modify the DOM, this causes the state of the DOM to no longer match the View from which it was rendered. When the renderer later tries to unmount the modified element, this can cause a crash.

This PR fixes the crash by catching (and ignoring) this failure in DOMRenderer.unmount. This fixes #326 and #369, which are the same issue.

Note that directly modifying the DOM with `_domRef` can still cause problems, as the state mismatch remains. For example, an update to the `View` can cause the renderer to overwrite those DOM changes.
2021-05-31 14:42:39 +01:00
Carson Katri 096ec5c4a2
Add multilineTextAlignment and use <br> to split spans up (#401) 2021-05-13 14:03:29 -04:00
Max Desiatov 57f57174c5 Fix typo in `CHANGELOG.md` 2021-05-03 12:14:52 +01:00
Max Desiatov 4a372c3fa8
Update `CHANGELOG.md` for the 0.7.0 release (#398)
Time for a new release!
2021-05-03 12:08:04 +01:00
Carson Katri d914e1bdc9
Add dynamicMemberLookup attribute to Binding (#396) 2021-04-09 09:58:03 -04:00
Emil 5c458f92b8
Add `DatePicker` to the `TokamakDOM` module (#394)
This fixes #320 by adding a SwiftUI-compatible `DatePicker`. However, `DatePickerStyle` is not supported. 

This uses the HTML inputs `date`, `time`, or `datetime-local`, depending on the given `displayedComponents`. This means that not all browsers show the picker, as Mac Safari currently does not support them. Safari on Mac will just show an ISO-format text field. If the date is in an invalid format, the binding will not receive updates until it becomes parseable by JSDate.

On supported browsers, the binding gets updated in real time, as you would expect, with a Foundation.Date, just like SwiftUI.

* Add DatePicker to TokamakCore and TokamakDOM

* Fix crash on invalid date

* Update progress.md and add credit

* Fix time zone related issues with the DatePicker

* Add DatePickerDemo to the TokamakDemo

* Fix overview for DatePicker

* Fix NativeDemo build
2021-03-28 21:32:29 +01:00
Max Desiatov bde7de9be0
Use `String(reflecting:)` vs `String(describing:)` (#391)
Resolves #218.

`String(describing:)` initializer applied to metatypes does not include a module name, which can cause problems if two different types with same name come from different modules.

OTOH `String(reflecting:)` does include module name, which makes these reflection strings slightly longer, but should prevent obscure issues with name collisions from happening.
2021-03-20 15:42:54 +00:00
Max Desiatov 8076035120
Clarify the difference between HTML and DynamicHTML (#389)
Resolves #388.
2021-03-11 10:21:48 +00:00
filip-sakel 4d211af563
Add '_spi(TokamakCore)' to ideally internal public members. (#386)
Adds `_spi(TokamakCore)` to the modules:
1. TokamakCore
2. TokamakDOM
3. TokamakGTK
4. TokamakStaticHTML

The attribute is applied to:
1. All `View` bodies in TokamakCore — either primitive or regular like `Color`
2. `ViewDeferredToRenderer` bodies
4. `ParentView` `children` members
5. `View` modifiers (such as  `_onMount(perform:)`)
6. Other members of types (like `Color._withScheme`, and `_AnyApp._launch`) 

The attribute semantics (from my brief testing)
1. It can only be applied to `public` declarations
2. It ensures that every SPI declaration is exposed only by other SPI declarations (i.e. `@_spi(Module) public enum A {}; public var a: A` is illegal)
3. Regularly importing a library prohibits clients from accessing SPI declarations that are not protocol witnesses (with an error).
4. Regularly importing a library "discourages" clients from accessing SPI protocol witnesses by hiding them from the autocompletion suggestions (i.e. users can still access `body` of `Text`, but autocompletion hides it).
5. For a declaration marked with `@_spi(Module)`, a client has to write `@_spi(Module) import Library` in order to normally access such SPI declarations.

* Add '_spi(TokamakCore)' to ideally internal public members.

* Remove `_spi` attribute on '_ConditionalContent'.

* Remove spi from 'Path._PathBox', 'Font._Font', and '_TupleScene.body'.

* Remove spi from types.

* Remove trailing whitespace.

* Apply spi to 'View' modifiers.

* Add _spi imports.

* Introduce 'PrimitiveView'.

* Remove 'PrimitiveView' conformances outside of TokamakCore.

* Fix `PrimitiveView` default implementation.

* Remove "BubbleCore" references.
2021-03-08 18:49:05 +00:00
Max Desiatov 4a1101c21a
Delete unused test.sh file 2021-02-23 19:51:12 +00:00
Morten Bek Ditlevsen 19cf8b6782
Made properties of CGPoint, CGSize and CGRect vars instead of lets. Initialized array used by CGAffineTransform concatenation (#382) 2021-02-18 08:04:37 +01:00
Max Desiatov eeddfe9e4b
Use immediate scheduler in TestRenderer (#380)
* Use immediate scheduler in TestRenderer

This allows running our test suite on WASI too, which doesn't have Dispatch and also can't wait on XCTest expectations. Previously none of our tests (especially runtime reflection tests) ran on WASI.

* Run `carton test` and `carton bundle` in separate jobs

* Bump year in the `LICENSE` file
2021-02-15 11:36:23 +00:00
414 changed files with 27150 additions and 3198 deletions

View File

@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: bug
assignees: MaxDesiatov assignees: carson-katri
--- ---
@ -28,17 +28,17 @@ A clear and concise description of what you expected to happen.
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference. If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. macOS] - OS: [e.g. macOS 12.4]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Version of the browser [e.g. 22] - Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.6.1] - Version of Tokamak [e.g. 0.10.1]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6] - Device: [e.g. iPhone 6]
- OS: [e.g. iOS8.1] - OS: [e.g. iOS15.1]
- Browser [e.g. stock browser, safari] - Browser [e.g. stock browser, safari]
- Version of the browser [e.g. 22] - Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.6.1] - Version of Tokamak [e.g. 0.10.1]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@ -6,17 +6,39 @@ on:
branches: [main] branches: [main]
jobs: jobs:
swiftwasm_build: swiftwasm_bundle_5_6:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.3 - uses: swiftwasm/swiftwasm-action@v5.6
with: with:
shell-action: carton bundle --product TokamakDemo shell-action: carton bundle --product TokamakDemo
- name: Check binary size
shell: bash
run: |
ls -la Bundle
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
swiftwasm_test:
runs-on: ubuntu-20.04
strategy:
fail-fast: true
matrix:
include:
- { toolchain: wasm-5.6.0-RELEASE }
- { toolchain: wasm-5.7-SNAPSHOT-2022-07-27-a }
- { toolchain: wasm-DEVELOPMENT-SNAPSHOT-2022-07-23-a }
steps:
- uses: actions/checkout@v2
- run: echo "${{ matrix.toolchain }}" > .swift-version
- uses: swiftwasm/swiftwasm-action@v5.6
with:
shell-action: carton test --environment node
core_macos_build: core_macos_build:
runs-on: macos-11.0 runs-on: macos-12
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -24,16 +46,17 @@ jobs:
shell: bash shell: bash
run: | run: |
set -ex set -ex
sudo xcode-select --switch /Applications/Xcode_12.3.app/Contents/Developer/ sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
# avoid building unrelated products for testing by specifying the test product explicitly # avoid building unrelated products for testing by specifying the test product explicitly
swift build --product TokamakPackageTests swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
rm -rf Sources/TokamakGTKCHelpers/*.c rm -rf Sources/TokamakGTKCHelpers/*.c
xcodebuild -version xcodebuild -version
# make sure Tokamak can be built on macOS so that Xcode autocomplete works # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \ xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
xcpretty --color xcpretty --color
@ -46,23 +69,33 @@ jobs:
./benchmark.sh ./benchmark.sh
gtk_macos_build: - name: Upload failed snapshots
runs-on: macos-latest uses: actions/upload-artifact@v2
if: ${{ failure() }}
with:
name: Failed snapshots
path: '*Tests'
steps: # FIXME: disabled due to build errors, to be investigated
- uses: actions/checkout@v2 # gtk_macos_build:
- name: Build the GTK renderer on macOS # runs-on: macos-12
shell: bash #
run: | # steps:
set -ex # - uses: actions/checkout@v2
sudo xcode-select --switch /Applications/Xcode_12.3.app/Contents/Developer/ # - name: Build the GTK renderer on macOS
# shell: bash
brew install gtk+3 # run: |
# set -ex
make build # sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
#
# brew install gtk+3
#
# make build
gtk_ubuntu_18_04_build: gtk_ubuntu_18_04_build:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
container:
image: swiftlang/swift:nightly-5.7-bionic
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -70,13 +103,14 @@ jobs:
shell: bash shell: bash
run: | run: |
set -ex set -ex
sudo apt-get update apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
sudo apt-get install libgtk+-3.0 gtk+-3.0
make build make build
gtk_ubuntu_20_04_build: gtk_ubuntu_20_04_build:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
container:
image: swiftlang/swift:nightly-5.7-focal
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -84,7 +118,6 @@ jobs:
shell: bash shell: bash
run: | run: |
set -ex set -ex
sudo apt-get update apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libgtk+-3.0 gtk+-3.0
sudo apt-get install libgtk+-3.0 gtk+-3.0
make build make build

View File

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

View File

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

View File

@ -12,7 +12,13 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
# Fetch current versions of files
- name: Fetch base ref
run: |
git fetch --prune --no-tags --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/heads/${{ github.base_ref }}
# Diff pull request to current files, then SwiftLint changed files
- name: GitHub Action for SwiftLint - name: GitHub Action for SwiftLint
uses: norio-nomura/action-swiftlint@3.1.0 uses: mayk-it/action-swiftlint@3.2.2
env: env:
DIFF_BASE: ${{ github.base_ref }} DIFF_BASE: ${{ github.base_ref }}
DIFF_HEAD: HEAD

3
.gitignore vendored
View File

@ -41,3 +41,6 @@ Pods/
# SwiftPM # SwiftPM
.build .build
/Packages /Packages
# VS Code
.vscode/launch.json

View File

@ -1,17 +1,18 @@
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
exclude: __Snapshots__
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0 rev: v2.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- id: detect-private-key - id: detect-private-key
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/hodovani/pre-commit-swift - repo: https://github.com/hodovani/pre-commit-swift
rev: master rev: master
hooks: hooks:
- id: swift-lint - id: swift-lint
- id: swift-format - id: swift-format

1
.swift-version Normal file
View File

@ -0,0 +1 @@
wasm-5.6.0-RELEASE

View File

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

17
.vscode/launch.json vendored
View File

@ -1,17 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "make",
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/.build/debug/TokamakGTKDemo",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -1,3 +1,195 @@
# 0.10.1 (20 May 2022)
This is a small bugfix release, which updates JavaScriptKit dependency to 0.15 and required version of `carton` to 0.15.
**Merged pull requests:**
- Update JSKit dependency ([#482](https://github.com/TokamakUI/Tokamak/pull/482)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Explicitly mention `carton` version in "Requirements" ([#481](https://github.com/TokamakUI/Tokamak/pull/481)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use stable `v5.6` version of `swiftwasm-action` ([#477](https://github.com/TokamakUI/Tokamak/pull/477)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.10.0 (9 April 2022)
This release adds support for SwiftWasm 5.6. It also updates JavaScriptKit and OpenCombineJS dependencies.
Due to issues with support for older SwiftWasm releases in the `carton`/SwiftPM integration, Tokamak now requires
SwiftWasm 5.6 or later, while SwiftWasm 5.4 and 5.5 are no longer supported.
**Merged pull requests:**
- Build and test with SwiftWasm 5.6 on CI ([#475](https://github.com/TokamakUI/Tokamak/pull/475)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.9.1 (16 February 2022)
This release fixes an issue with `EnvironmentValues`, updates CI workflow for SwiftWasm 5.5, and bumps JavaScriptKit dependency to 0.12.0.
**Merged pull requests:**
- Fix typo ([#462](https://github.com/TokamakUI/Tokamak/pull/462)) via [@regexident](https://github.com/regexident)
- Fix `rootEnvironment` not merged with `.defaultEnvironment` ([#461](https://github.com/TokamakUI/Tokamak/pull/461)) via [@regexident](https://github.com/regexident)
- Build and test with SwiftWasm 5.5 on CI ([#460](https://github.com/TokamakUI/Tokamak/pull/460)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.9.0 (26 November 2021)
This release adds support for SwiftWasm 5.5 and bumps the minimum required version to Swift 5.4.
It now depends on JavaScriptKit 0.11.1, which no longer requires manual memory management of
`JSClosure` instances. The downside of that update is that minimum browser version requirements are
significantly higher now. See [`README.md`](README.md#requirements) for more details.
Additionally, a few new features were added to the DOM renderer:
- `Canvas` and `TimelineView`;
- `onHover` modifier;
- `task` modifier for running `async` functions;
- Sanitizers for `Text` view.
Many thanks (in alphabetical order) to [@agg23](https://github.com/agg23),
[@carson-katri](https://github.com/carson-katri), [@ezraberch](https://github.com/ezraberch),
and [@mbrandonw](https://github.com/mbrandonw) for their contributions to this release!
**Closed issues:**
- `TextField` Not Rendering the field ([#455](https://github.com/TokamakUI/Tokamak/issues/455))
- Can't find `CGSize` or `CGFloat` type ([#450](https://github.com/TokamakUI/Tokamak/issues/450))
- `UnitPoint` constants don't match SwiftUI ([#443](https://github.com/TokamakUI/Tokamak/issues/443))
**Merged pull requests:**
- Update for JSKit 0.11.1, add async `task` modifier ([#457](https://github.com/TokamakUI/Tokamak/pull/457)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Switch to Xcode 13.0 in `gtk_macos_build` job ([#454](https://github.com/TokamakUI/Tokamak/pull/454)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `Canvas` and `TimelineView` to DOM renderer ([#449](https://github.com/TokamakUI/Tokamak/pull/449)) via [@carson-katri](https://github.com/carson-katri)
- Initial implementation of `onHover` ([#448](https://github.com/TokamakUI/Tokamak/pull/448)) via [@agg23](https://github.com/agg23)
- Refactor `NavigationView` ([#446](https://github.com/TokamakUI/Tokamak/pull/446)) via [@ezraberch](https://github.com/ezraberch)
- Save HTML snapshots with .html extension. ([#447](https://github.com/TokamakUI/Tokamak/pull/447)) via [@mbrandonw](https://github.com/mbrandonw)
- Add HTML renderer support for AngularGradient ([#444](https://github.com/TokamakUI/Tokamak/pull/444)) via [@ezraberch](https://github.com/ezraberch)
- Bump requirements to Swift 5.4, migrate to `@resultBuilder` ([#442](https://github.com/TokamakUI/Tokamak/pull/442)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add HTML sanitizer to `Text` ([#437](https://github.com/TokamakUI/Tokamak/pull/437)) via [@ezraberch](https://github.com/ezraberch)
- Add `@ezraberch` to the list of maintainers ([#440](https://github.com/TokamakUI/Tokamak/pull/440)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.8.0 (17 August 2021)
This release adds support for more SwiftUI types and modifiers, and fixes bugs. Including, but not
limited to:
- `Toolbar` type and `toolbar` modifier
- `ProgressView` type
- `Animation` and related types and modifiers
- `opacity`, `scaleEffect`, `aspectRatio`, and `controlSize` modifiers
- `Material` and `Gradient` types
- `HierarchicalShapeStyle` (`.primary`/`.secondary`/`.tertiary`/`.quaternary`) type
- `ContainerRelativeShape` type
- `spacing` argument support for initializers of `HStack` and `VStack`
- support for standard Foundation types, such as `CGRect`, `CGSize` (we previously used our own
implementation of those, which weren't fully compatible with Foundation)
- ability to sort HTML attributes when generating static HTML, which is essential for end-to-end
tests that cover generated output.
Many thanks to [@carson-katri](https://github.com/carson-katri),
[@ezraberch](https://github.com/ezraberch), and [@yonihemi](https://github.com/yonihemi) for
their contributions to this release!
**Closed issues:**
- Is there anyway to compile this from Xcode? ([#406](https://github.com/TokamakUI/Tokamak/issues/406))
- Xcode doesn't compile — gtk/gtk.h not found ([#405](https://github.com/TokamakUI/Tokamak/issues/405))
- Use `NSGeometry` types from Foundation ([#404](https://github.com/TokamakUI/Tokamak/issues/404))
- Adding padding to a view contained in a Button causes the Button to disappear ([#403](https://github.com/TokamakUI/Tokamak/issues/403))
- .background modifier with contained shape causes view to expand to full vertical size of the screen ([#402](https://github.com/TokamakUI/Tokamak/issues/402))
- Multi-line string handling in Text views ([#400](https://github.com/TokamakUI/Tokamak/issues/400))
- Content with spacer jumps when blurring and focusing the page ([#395](https://github.com/TokamakUI/Tokamak/issues/395))
- Frame sizes do not match expected behavior. ([#387](https://github.com/TokamakUI/Tokamak/issues/387))
- URL hash change demo crashes ([#369](https://github.com/TokamakUI/Tokamak/issues/369))
- Infinite loops w/ 100% CPU usage caused by stack overflows ([#367](https://github.com/TokamakUI/Tokamak/issues/367))
- TokamakDemo breaks after use of `_domRef` ([#326](https://github.com/TokamakUI/Tokamak/issues/326))
- Add support for `toolbar` modifier and related types ([#316](https://github.com/TokamakUI/Tokamak/issues/316))
**Merged pull requests:**
- Revise `ShapeStyle` and add `Gradient`s ([#435](https://github.com/TokamakUI/Tokamak/pull/435)) via [@carson-katri](https://github.com/carson-katri)
- Add `Toolbar` implementation for HTML renderer ([#169](https://github.com/TokamakUI/Tokamak/pull/169)) via [@carson-katri](https://github.com/carson-katri)
- Fix SwiftLint action ([#434](https://github.com/TokamakUI/Tokamak/pull/434)) via [@ezraberch](https://github.com/ezraberch)
- Add View Traits and transitions ([#426](https://github.com/TokamakUI/Tokamak/pull/426)) via [@carson-katri](https://github.com/carson-katri)
- Add `ToolbarItem` and its builder functions ([#430](https://github.com/TokamakUI/Tokamak/pull/430)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `controlSize`/`controlProminence` modifiers ([#431](https://github.com/TokamakUI/Tokamak/pull/431)) via [@carson-katri](https://github.com/carson-katri)
- Fix background/overlay layout in DOM/HTML renderers ([#429](https://github.com/TokamakUI/Tokamak/pull/429)) via [@carson-katri](https://github.com/carson-katri)
- Add `ProgressView` ([#425](https://github.com/TokamakUI/Tokamak/pull/425)) via [@carson-katri](https://github.com/carson-katri)
- Add support for custom fonts ([#421](https://github.com/TokamakUI/Tokamak/pull/421)) via [@carson-katri](https://github.com/carson-katri)
- Animation implementation using the Web Animations API ([#427](https://github.com/TokamakUI/Tokamak/pull/427)) via [@carson-katri](https://github.com/carson-katri)
- Add `scaleEffect` modifier ([#424](https://github.com/TokamakUI/Tokamak/pull/424)) via [@carson-katri](https://github.com/carson-katri)
- Add `aspectRatio` modifier ([#422](https://github.com/TokamakUI/Tokamak/pull/422)) via [@carson-katri](https://github.com/carson-katri)
- Check minWidth/Height == nil ([#420](https://github.com/TokamakUI/Tokamak/pull/420)) via [@carson-katri](https://github.com/carson-katri)
- Add Primary/Secondary/Tertiary/QuaternaryContentStyle ([#419](https://github.com/TokamakUI/Tokamak/pull/419)) via [@carson-katri](https://github.com/carson-katri)
- Add `Material` to the HTML renderer ([#418](https://github.com/TokamakUI/Tokamak/pull/418)) via [@carson-katri](https://github.com/carson-katri)
- Improve ShapeStyles to match iOS 15+ ([#417](https://github.com/TokamakUI/Tokamak/pull/417)) via [@carson-katri](https://github.com/carson-katri)
- Add ContainerRelativeShape ([#416](https://github.com/TokamakUI/Tokamak/pull/416)) via [@carson-katri](https://github.com/carson-katri)
- Add HTML implementation for `opacity` modifier ([#415](https://github.com/TokamakUI/Tokamak/pull/415)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Support `spacing` property on `HStack`/`VStack` ([#273](https://github.com/TokamakUI/Tokamak/pull/273)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Explicitly import CoreFoundation ([#413](https://github.com/TokamakUI/Tokamak/pull/413)) via [@yonihemi](https://github.com/yonihemi)
- Fix handling of stroked shapes ([#414](https://github.com/TokamakUI/Tokamak/pull/414)) via [@ezraberch](https://github.com/ezraberch)
- Add a snapshot test for `Path` SVG layout ([#412](https://github.com/TokamakUI/Tokamak/pull/412)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Attempt `padding` modifier fusion to avoid nested `div`s ([#253](https://github.com/TokamakUI/Tokamak/pull/253)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use `CGFloat`, `CGPoint`, `CGRect` from Foundation ([#411](https://github.com/TokamakUI/Tokamak/pull/411)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add reconciler stress tests for elaborate testing ([#381](https://github.com/TokamakUI/Tokamak/pull/381)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix spacers after `DOMRenderer.update` ([#410](https://github.com/TokamakUI/Tokamak/pull/410)) via [@ezraberch](https://github.com/ezraberch)
- Replace `ViewDeferredToRenderer`, fix renderer tests ([#408](https://github.com/TokamakUI/Tokamak/pull/408)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Allow DOMRenderer to render buttons with non-Text labels (#403) ([#409](https://github.com/TokamakUI/Tokamak/pull/409)) via [@ezraberch](https://github.com/ezraberch)
- Sort attributes in HTML nodes when rendering ([#346](https://github.com/TokamakUI/Tokamak/pull/346)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.7.0 (3 May 2021)
This release introduces new view types such as `DatePicker`, new modifiers such as `shadow`,
improves test coverage, updates dependencies, and fixes multiple bugs and crashes. Additionally,
a proof of concept GTK renderer is now available in the `TokamakGTK` module.
Many thanks to (in alphabetical order)
[@carson-katri](https://github.com/carson-katri), [@filip-sakel](https://github.com/filip-sakel),
[@foscomputerservices](https://github.com/foscomputerservices), [@literalpie](https://github.com/literalpie),
[@mattpolzin](https://github.com/mattpolzin), [@mortenbekditlevsen](https://github.com/mortenbekditlevsen),
and [@Snowy1803](https://github.com/Snowy1803) for their contributions to this release!
**Closed issues:**
- `@ObservedObject` is a get-only property ([#392](https://github.com/TokamakUI/Tokamak/issues/392))
- What is the difference between `HTML` and `DynamicHTML`? ([#388](https://github.com/TokamakUI/Tokamak/issues/388))
- Reduce `View.body` Visibility ([#385](https://github.com/TokamakUI/Tokamak/issues/385))
- Verify that type constructor names contain contain module names ([#368](https://github.com/TokamakUI/Tokamak/issues/368))
- Crash when using a `View` with optional content ([#362](https://github.com/TokamakUI/Tokamak/issues/362))
- Set up code coverage reports on GitHub Actions ([#350](https://github.com/TokamakUI/Tokamak/issues/350))
- Shadow support ([#324](https://github.com/TokamakUI/Tokamak/issues/324))
- Implement `DatePicker` view in the DOM renderer ([#320](https://github.com/TokamakUI/Tokamak/issues/320))
- `TokamakDemo` build failed ([#305](https://github.com/TokamakUI/Tokamak/issues/305))
**Merged pull requests:**
- Add `@dynamicMemberLookup` to `Binding` ([#396](https://github.com/TokamakUI/Tokamak/pull/396)) via [@carson-katri](https://github.com/carson-katri)
- Add `DatePicker` to the `TokamakDOM` module ([#394](https://github.com/TokamakUI/Tokamak/pull/394)) via [@Snowy1803](https://github.com/Snowy1803)
- Use `String(reflecting:)` vs `String(describing:)` ([#391](https://github.com/TokamakUI/Tokamak/pull/391)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Clarify the difference between `HTML` and `DynamicHTML` ([#389](https://github.com/TokamakUI/Tokamak/pull/389)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `_spi(TokamakCore)` to ideally internal public members ([#386](https://github.com/TokamakUI/Tokamak/pull/386)) via [@filip-sakel](https://github.com/filip-sakel)
- Make properties of `CGPoint`, `CGSize` and `CGRect` `var`s instead of `let`s ([#382](https://github.com/TokamakUI/Tokamak/pull/382)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Use immediate scheduler in `TestRenderer` ([#380](https://github.com/TokamakUI/Tokamak/pull/380)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Simple Code Coverage analysis ([#378](https://github.com/TokamakUI/Tokamak/pull/378)) via [@mattpolzin](https://github.com/mattpolzin)
- Add checks for metadata state ([#375](https://github.com/TokamakUI/Tokamak/pull/375)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use upstream OpenCombine instead of a fork ([#377](https://github.com/TokamakUI/Tokamak/pull/377)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Update JavaScriptKit, OpenCombineJS dependencies ([#376](https://github.com/TokamakUI/Tokamak/pull/376)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Clean up metadata reflection code ([#372](https://github.com/TokamakUI/Tokamak/pull/372)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add David Hunt to the list of maintainers ([#373](https://github.com/TokamakUI/Tokamak/pull/373)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Refactor environment injection, add a test ([#371](https://github.com/TokamakUI/Tokamak/pull/371)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Replace uses of the Runtime library with stdlib ([#370](https://github.com/TokamakUI/Tokamak/pull/370)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use `macos-latest` agent for the GTK build ([#360](https://github.com/TokamakUI/Tokamak/pull/360)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add a benchmark target and a script to run it ([#365](https://github.com/TokamakUI/Tokamak/pull/365)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix crashes in views with optional content ([#364](https://github.com/TokamakUI/Tokamak/pull/364)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add GTK support for `SecureField` ([#363](https://github.com/TokamakUI/Tokamak/pull/363)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Add support for shadow modifier ([#355](https://github.com/TokamakUI/Tokamak/pull/355)) via [@literalpie](https://github.com/literalpie)
- Two infinite loop fixes ([#359](https://github.com/TokamakUI/Tokamak/pull/359)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Added `TextField` support for GTK using `GtkEntry` ([#361](https://github.com/TokamakUI/Tokamak/pull/361)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Fixed a small issue with re-renderers being dropped ([#356](https://github.com/TokamakUI/Tokamak/pull/356)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Removed an extra space that cause Safari to issue "Invalid value" ([#358](https://github.com/TokamakUI/Tokamak/pull/358)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Add `@mortenbekditlevsen` to the list of maintainers in `README.md` ([#352](https://github.com/TokamakUI/Tokamak/pull/352)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Build the GTK renderer on Ubuntu on CI ([#347](https://github.com/TokamakUI/Tokamak/pull/347)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add missing `Link` re-export to TokamakDOM ([#351](https://github.com/TokamakUI/Tokamak/pull/351)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- GTK shape support WIP ([#348](https://github.com/TokamakUI/Tokamak/pull/348)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Add a "bug report" issue template ([#349](https://github.com/TokamakUI/Tokamak/pull/349)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.6.1 (6 December 2020) # 0.6.1 (6 December 2020)
This release fixes autocomplete in Xcode for projects that depend on Tokamak. This release fixes autocomplete in Xcode for projects that depend on Tokamak.

70
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,70 @@
### Modular structure
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and
separate modules for platform-specific renderers. Currently, the only available renderer modules are
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers
guide](docs/RenderersGuide.md) for more details.
Tokamak users only need to import a renderer module they would like to use, while
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
those symbols with underscores in their names to indicate this. It can be formulated as these
"rules":
1. If a symbol is restricted to a module and has no `public` access control, no need for an
underscore.
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
renderer module via `public typealias`.
3. If a function or a type have `public` on them only by necessity to make them available in
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
indicate that.
The benefit of separate modules is that they allow us to provide separate renderers for different
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
`TokamakAndroid` with no need to be aware of any of the web modules.
### Testing
Tokamak uses [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) library to
make sure that HTML and layout generated by renderers stay consistent. To run the test suite on macOS
you should use this command:
```sh
swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest
```
Unfortunately, plain `swift test` won't work as it tries to build targets that aren't related to
the test suite.
### Coding Style
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
[SwiftLint](https://github.com/realm/SwiftLint) to enforce formatting and coding style. SwiftFormat
0.45.3 and SwiftLint 0.39.2 or later versions are recommended. We encourage you to run SwiftFormat
and SwiftLint within a local clone of the repository in whatever way works best for you. You can do
that either manually, or automatically with VSCode extensions for
[SwiftFormat](https://github.com/vknabel/vscode-swiftformat) and
[SwiftLint](https://github.com/vknabel/vscode-swiftlint) respectively, or with the [Xcode
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension), or [build
phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase).
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run
this once to set up the [pre-commit](https://pre-commit.com/) hook:
```
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit
```
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
and installation instructions for other platforms.
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
fail with inconsistent formatting or style. We require CI builds to pass for all
PRs before merging.

View File

@ -186,7 +186,7 @@ file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2018-2020 Digital Signal Limited and Tokamak Contributors Copyright 2018-2021 Digital Signal Limited and Tokamak Contributors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -7,59 +7,75 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */; };
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; }; 85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; };
85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.storyboard */; }; 85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.storyboard */; };
85ED188C24AD3CF10085DFA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */; }; 85ED188C24AD3CF10085DFA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */; };
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; }; 85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; }; 85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; }; 85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; }; D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; }; D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; }; D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; }; D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; }; D1078752274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; }; D1078753274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; }; D1078754274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; }; D1078755274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; }; D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; }; D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; }; D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; }; D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; }; D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */; }; D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; }; D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; }; D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; }; D1078762274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; }; D1078763274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
D107878A274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
D107878B274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; }; D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; }; D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; }; D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -92,40 +108,48 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; }; 8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
85ED185224AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 85ED185224AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAppDelegate.swift; sourceTree = "<group>"; }; 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAppDelegate.swift; sourceTree = "<group>"; };
85ED188724AD3CC30085DFA0 /* macOS.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = macOS.storyboard; sourceTree = "<group>"; }; 85ED188724AD3CC30085DFA0 /* macOS.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = macOS.storyboard; sourceTree = "<group>"; };
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAppDelegate.swift; sourceTree = "<group>"; }; 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAppDelegate.swift; sourceTree = "<group>"; };
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; }; 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
85ED189B24AD425E0085DFA0 /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; };
85ED189E24AD425E0085DFA0 /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; }; 85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; }; D1078726274BD1E5003E787B /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
B56F22DF24BC89FD001738DF /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; tabWidth = 2; }; D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
B56F22E224BD1C26001738DF /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; }; D1078728274BD1E5003E787B /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; }; D1078729274BD1E5003E787B /* StackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackDemo.swift; sourceTree = "<group>"; };
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; }; D107872B274BD1E5003E787B /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; }; D107872C274BD1E5003E787B /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; }; D107872D274BD1E5003E787B /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; }; D107872E274BD1E5003E787B /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; }; D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; }; D1078731274BD1E5003E787B /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; }; D1078732274BD1E5003E787B /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1078733274BD1E5003E787B /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
D1078735274BD1E5003E787B /* TaskDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDemo.swift; sourceTree = "<group>"; };
D1078736274BD1E5003E787B /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
D107873B274BD1E5003E787B /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
D107873C274BD1E5003E787B /* CanvasDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasDemo.swift; sourceTree = "<group>"; };
D107873D274BD1E5003E787B /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; };
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
D1078740274BD1E5003E787B /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
D1078742274BD1E5003E787B /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
D1078744274BD1E5003E787B /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
D1078746274BD1E5003E787B /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
D1078748274BD1E5003E787B /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
D1078749274BD1E5003E787B /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; };
D107874A274BD1E5003E787B /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
D107874C274BD1E5003E787B /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; }; D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; }; D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -185,34 +209,106 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = { 85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */, D107874B274BD1E5003E787B /* Buttons */,
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */, D107872F274BD1E5003E787B /* Containers */,
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */, D107873A274BD1E5003E787B /* Drawing */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */, D1078725274BD1E5003E787B /* Layout */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */, D107873F274BD1E5003E787B /* Misc */,
85ED189E24AD425E0085DFA0 /* Counter.swift */, D1078734274BD1E5003E787B /* Modifiers */,
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */, D107872A274BD1E5003E787B /* Selectors */,
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */, D1078747274BD1E5003E787B /* Text */,
B56F22E224BD1C26001738DF /* GridDemo.swift */,
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */,
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
B51F214F24B920B400CF2583 /* PathDemo.swift */,
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */,
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */,
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */,
85ED189B24AD425E0085DFA0 /* TextDemo.swift */,
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */,
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */, 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */,
4550BD5125B642B80088F4EA /* ShadowDemo.swift */,
); );
name = TokamakDemo; name = TokamakDemo;
path = ../Sources/TokamakDemo; path = ../Sources/TokamakDemo;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D1078725274BD1E5003E787B /* Layout */ = {
isa = PBXGroup;
children = (
D1078726274BD1E5003E787B /* SpacerDemo.swift */,
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */,
D1078728274BD1E5003E787B /* GridDemo.swift */,
D1078729274BD1E5003E787B /* StackDemo.swift */,
);
path = Layout;
sourceTree = "<group>";
};
D107872A274BD1E5003E787B /* Selectors */ = {
isa = PBXGroup;
children = (
D107872B274BD1E5003E787B /* DatePickerDemo.swift */,
D107872C274BD1E5003E787B /* SliderDemo.swift */,
D107872D274BD1E5003E787B /* PickerDemo.swift */,
D107872E274BD1E5003E787B /* ToggleDemo.swift */,
);
path = Selectors;
sourceTree = "<group>";
};
D107872F274BD1E5003E787B /* Containers */ = {
isa = PBXGroup;
children = (
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */,
D1078731274BD1E5003E787B /* ForEachDemo.swift */,
D1078732274BD1E5003E787B /* ListDemo.swift */,
D1078733274BD1E5003E787B /* SidebarDemo.swift */,
);
path = Containers;
sourceTree = "<group>";
};
D1078734274BD1E5003E787B /* Modifiers */ = {
isa = PBXGroup;
children = (
D1078735274BD1E5003E787B /* TaskDemo.swift */,
D1078736274BD1E5003E787B /* ShadowDemo.swift */,
);
path = Modifiers;
sourceTree = "<group>";
};
D107873A274BD1E5003E787B /* Drawing */ = {
isa = PBXGroup;
children = (
D107873B274BD1E5003E787B /* PathDemo.swift */,
D107873C274BD1E5003E787B /* CanvasDemo.swift */,
D107873D274BD1E5003E787B /* ColorDemo.swift */,
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */,
);
path = Drawing;
sourceTree = "<group>";
};
D107873F274BD1E5003E787B /* Misc */ = {
isa = PBXGroup;
children = (
D1078740274BD1E5003E787B /* AnimationDemo.swift */,
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */,
D1078742274BD1E5003E787B /* TransitionDemo.swift */,
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */,
D1078744274BD1E5003E787B /* AppStorageDemo.swift */,
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */,
D1078746274BD1E5003E787B /* RedactDemo.swift */,
);
path = Misc;
sourceTree = "<group>";
};
D1078747274BD1E5003E787B /* Text */ = {
isa = PBXGroup;
children = (
D1078748274BD1E5003E787B /* TextDemo.swift */,
D1078749274BD1E5003E787B /* TextEditorDemo.swift */,
D107874A274BD1E5003E787B /* TextFieldDemo.swift */,
);
path = Text;
sourceTree = "<group>";
};
D107874B274BD1E5003E787B /* Buttons */ = {
isa = PBXGroup;
children = (
D107874C274BD1E5003E787B /* Counter.swift */,
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
D1E5FDAB24C1D57000E7485E /* TokamakShim */ = { D1E5FDAB24C1D57000E7485E /* TokamakShim */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -351,30 +447,38 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */,
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */,
D107878A274BD1E5003E787B /* Counter.swift in Sources */,
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */,
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */, 85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */, D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */, D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */,
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D120FDDB257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */,
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */, 85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */,
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */, D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */,
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */,
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */, D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */,
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */, D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */, D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */,
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */, D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */,
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */,
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */,
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */,
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */,
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */,
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */,
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */,
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */,
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -382,30 +486,38 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */,
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */,
D107878B274BD1E5003E787B /* Counter.swift in Sources */,
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */,
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */, 85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */, D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */, D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */,
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D120FDDC257E7145008FFBAD /* TextEditorDemo.swift in Sources */,
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */,
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */, 85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */,
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */, D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */,
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */, D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */,
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */, D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */,
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */, D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */,
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */, D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */,
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */,
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */,
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */,
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */,
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */,
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */,
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */,
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
D1078763274BD1E5003E787B /* ListDemo.swift in Sources */,
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -1,52 +1,59 @@
{ {
"object": { "pins" : [
"pins": [ {
{ "identity" : "javascriptkit",
"package": "JavaScriptKit", "kind" : "remoteSourceControl",
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git", "location" : "https://github.com/swiftwasm/JavaScriptKit.git",
"state": { "state" : {
"branch": null, "revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
"revision": "ebd9ca04215397f0e3cb72d6e96406a980a424e5", "version" : "0.15.0"
"version": "0.10.0"
}
},
{
"package": "OpenCombine",
"repositoryURL": "https://github.com/OpenCombine/OpenCombine.git",
"state": {
"branch": null,
"revision": "28993ae57de5a4ea7e164787636cafad442d568c",
"version": "0.12.0"
}
},
{
"package": "OpenCombineJS",
"repositoryURL": "https://github.com/swiftwasm/OpenCombineJS.git",
"state": {
"branch": null,
"revision": "eaf324ce78710f53b52fb82e9a8de4693633e33a",
"version": "0.1.1"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
"version": "0.3.2"
}
},
{
"package": "Benchmark",
"repositoryURL": "https://github.com/google/swift-benchmark",
"state": {
"branch": null,
"revision": "8e0ef8bb7482ab97dcd2cd1d6855bd38921c345d",
"version": "0.1.0"
}
} }
] },
}, {
"version": 1 "identity" : "opencombine",
"kind" : "remoteSourceControl",
"location" : "https://github.com/OpenCombine/OpenCombine.git",
"state" : {
"revision" : "9cf67e363738dbab61b47fb5eaed78d3db31e5ee",
"version" : "0.13.0"
}
},
{
"identity" : "opencombinejs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftwasm/OpenCombineJS.git",
"state" : {
"revision" : "e574e418ba468ff5c2d4c499eb56f108aeb4d2ba",
"version" : "0.2.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
"version" : "1.1.2"
}
},
{
"identity" : "swift-benchmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/swift-benchmark",
"state" : {
"revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
"version" : "0.1.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "f8a9c997c3c1dab4e216a8ec9014e23144cbab37",
"version" : "1.9.0"
}
}
],
"version" : 2
} }

View File

@ -1,6 +1,4 @@
// swift-tools-version:5.3 // swift-tools-version:5.6
// The swift-tools-version declares the minimum version of Swift required to
// build this package.
import PackageDescription import PackageDescription
@ -26,8 +24,8 @@ let package = Package(
targets: ["TokamakStaticHTML"] targets: ["TokamakStaticHTML"]
), ),
.executable( .executable(
name: "TokamakStaticDemo", name: "TokamakStaticHTMLDemo",
targets: ["TokamakStaticDemo"] targets: ["TokamakStaticHTMLDemo"]
), ),
.library( .library(
name: "TokamakGTK", name: "TokamakGTK",
@ -47,32 +45,40 @@ let package = Package(
), ),
], ],
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package( .package(
url: "https://github.com/swiftwasm/JavaScriptKit.git", url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
.upToNextMinor(from: "0.10.0") from: "0.15.0"
),
.package(
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
from: "0.12.0"
),
.package(
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
from: "0.2.0"
),
.package(
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
from: "0.1.2"
),
.package(
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
from: "1.9.0"
), ),
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0"),
.package(url: "https://github.com/swiftwasm/OpenCombineJS.git", .upToNextMinor(from: "0.1.1")),
.package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.0"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define // Targets are the basic building blocks of a package. A target can define
// a module or a test suite. // a module or a test suite.
// Targets can depend on other targets in this package, and on products // Targets can depend on other targets in this package, and on products
// in packages which this package depends on. // in packages which this package depends on.
.target(
name: "CombineShim",
dependencies: [.product(
name: "OpenCombine",
package: "OpenCombine",
condition: .when(platforms: [.wasi, .linux])
)]
),
.target( .target(
name: "TokamakCore", name: "TokamakCore",
dependencies: ["CombineShim"] dependencies: [
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
]
), ),
.target( .target(
name: "TokamakShim", name: "TokamakShim",
@ -105,9 +111,15 @@ let package = Package(
), ),
.target( .target(
name: "TokamakGTK", name: "TokamakGTK",
dependencies: ["TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers", "CombineShim"] dependencies: [
"TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers",
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
]
), ),
.target( .executableTarget(
name: "TokamakGTKDemo", name: "TokamakGTKDemo",
dependencies: ["TokamakGTK"], dependencies: ["TokamakGTK"],
resources: [.copy("logo-header.png")] resources: [.copy("logo-header.png")]
@ -118,35 +130,44 @@ let package = Package(
"TokamakCore", "TokamakCore",
] ]
), ),
.target( .executableTarget(
name: "TokamakCoreBenchmark", name: "TokamakCoreBenchmark",
dependencies: [ dependencies: [
"Benchmark", .product(name: "Benchmark", package: "swift-benchmark"),
"TokamakCore", "TokamakCore",
"TokamakTestRenderer",
] ]
), ),
.target( .executableTarget(
name: "TokamakStaticHTMLBenchmark", name: "TokamakStaticHTMLBenchmark",
dependencies: [ dependencies: [
"Benchmark", .product(name: "Benchmark", package: "swift-benchmark"),
"TokamakStaticHTML", "TokamakStaticHTML",
] ]
), ),
.target( .target(
name: "TokamakDOM", name: "TokamakDOM",
dependencies: [ dependencies: [
"CombineShim",
"OpenCombineJS",
"TokamakCore", "TokamakCore",
"TokamakStaticHTML", "TokamakStaticHTML",
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
.product( .product(
name: "JavaScriptKit", name: "JavaScriptKit",
package: "JavaScriptKit", package: "JavaScriptKit",
condition: .when(platforms: [.wasi]) condition: .when(platforms: [.wasi])
), ),
.product(
name: "JavaScriptEventLoop",
package: "JavaScriptKit",
condition: .when(platforms: [.wasi])
),
"OpenCombineJS",
] ]
), ),
.target( .executableTarget(
name: "TokamakDemo", name: "TokamakDemo",
dependencies: [ dependencies: [
"TokamakShim", "TokamakShim",
@ -156,10 +177,16 @@ let package = Package(
condition: .when(platforms: [.wasi]) condition: .when(platforms: [.wasi])
), ),
], ],
resources: [.copy("logo-header.png")] resources: [.copy("logo-header.png")],
linkerSettings: [
.unsafeFlags(
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
.when(platforms: [.wasi])
),
]
), ),
.target( .executableTarget(
name: "TokamakStaticDemo", name: "TokamakStaticHTMLDemo",
dependencies: [ dependencies: [
"TokamakStaticHTML", "TokamakStaticHTML",
] ]
@ -168,18 +195,40 @@ let package = Package(
name: "TokamakTestRenderer", name: "TokamakTestRenderer",
dependencies: ["TokamakCore"] dependencies: ["TokamakCore"]
), ),
.testTarget(
name: "TokamakLayoutTests",
dependencies: [
"TokamakCore",
"TokamakStaticHTML",
.product(
name: "SnapshotTesting",
package: "swift-snapshot-testing",
condition: .when(platforms: [.macOS])
),
]
),
.testTarget(
name: "TokamakReconcilerTests",
dependencies: [
"TokamakCore",
"TokamakTestRenderer",
]
),
.testTarget( .testTarget(
name: "TokamakTests", name: "TokamakTests",
dependencies: ["TokamakTestRenderer"] dependencies: ["TokamakTestRenderer"]
), ),
// FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved .testTarget(
// Currently, when multiple modules that have conflicting `ViewDeferredToRenderer` name: "TokamakStaticHTMLTests",
// implementations are linked in the same binary, only a single one is used with no defined dependencies: [
// behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution "TokamakStaticHTML",
// that isn't prone to these hard to debug errors. .product(
// .testTarget( name: "SnapshotTesting",
// name: "TokamakStaticHTMLTests", package: "swift-snapshot-testing",
// dependencies: ["TokamakStaticHTML"] condition: .when(platforms: [.macOS])
// ), ),
],
exclude: ["__Snapshots__", "RenderingTests/__Snapshots__"]
),
] ]
) )

191
README.md
View File

@ -52,6 +52,7 @@ struct Counter: View {
} }
} }
@main
struct CounterApp: App { struct CounterApp: App {
var body: some Scene { var body: some Scene {
WindowGroup("Counter Demo") { WindowGroup("Counter Demo") {
@ -59,10 +60,6 @@ struct CounterApp: App {
} }
} }
} }
// @main attribute is not supported in SwiftPM apps.
// See https://bugs.swift.org/browse/SR-12683 for more details.
CounterApp.main()
``` ```
### Arbitrary HTML ### Arbitrary HTML
@ -82,6 +79,40 @@ struct SVGCircle: View {
} }
``` ```
`HTML` doesn't support event listeners, and is declared in the `TokamakStaticHTML` module, which `TokamakDOM` re-exports. The benefit of `HTML` is that you can use it for static rendering in libraries like [TokamakVapor](https://github.com/TokamakUI/TokamakVapor) and [TokamakPublish](https://github.com/TokamakUI/TokamakPublish).
Another option is the `DynamicHTML` view provided by the `TokamakDOM` module, which has a `listeners` property with a corresponding initializer parameter. You can pass closures that can handle `onclick`, `onmouseover` and other DOM events for you in the `listeners` dictionary. Check out [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers) for the full list.
An example of mouse events handling with `DynamicHTML` would look like this:
```swift
struct MouseEventsView: View {
@State var position: CGPoint = .zero
@State var isMouseButtonDown: Bool = false
var body: some View {
DynamicHTML(
"div",
["style": "width: 200px; height: 200px; background-color: red;"],
listeners: [
"mousemove": { event in
guard
let x = event.offsetX.jsValue.number,
let y = event.offsetY.jsValue.number
else { return }
position = CGPoint(x: x, y: y)
},
"mousedown": { _ in isMouseButtonDown = true },
"mouseup": { _ in isMouseButtonDown = false },
]
) {
Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
}
}
}
```
### Arbitrary styles and scripts ### Arbitrary styles and scripts
While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript, While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript,
@ -107,23 +138,63 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
localized date formatting (or any arbitrary style/script/font added that way) are available in your localized date formatting (or any arbitrary style/script/font added that way) are available in your
app. app.
## Requirements for app developers ### Fiber renderers
- macOS 10.15 and Xcode 11.4 or later. macOS 11.0 and Xcode 12.0 or later are required if you're A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
building a multi-platform app with Tokamak that also needs to support SwiftUI on macOS. is optionally available. It can provide faster updates and allow for larger View hierarchies.
- [Swift 5.2 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use Linux. It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
You can specify which reconciler to use in your `App`'s configuration:
```swift
struct CounterApp: App {
static let _configuration: _AppConfiguration = .init(
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
reconciler: .fiber(useDynamicLayout: true)
)
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
```
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
## Requirements
### For app developers
- macOS 11 and Xcode 13.2 or later when using VS Code. macOS 12 and Xcode 13.3 or later are recommended if
you'd like to use Xcode for auto-completion, or when developing multi-platform apps that target WebAssembly
and macOS at the same time.
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
Other Linux distributions are currently not supported. Other Linux distributions are currently not supported.
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
## Requirements for app users ### For users of apps depending on Tokamak
Any browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) should work, which currently includes: Any recent browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) and [required
JavaScript features](https://caniuse.com/?search=finalizationregistry) should work, which currently includes:
- Edge 84+
- Firefox 79+
- Chrome 84+
- Desktop Safari 14.1+
- Mobile Safari 14.8+
If you need to support older browser versions, you'll have to build with
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags
when compiling. This should lower browser requirements to these versions:
- Edge 16+ - Edge 16+
- Firefox 53+ - Firefox 61+
- Chrome 57+ - Chrome 66+
- (Mobile) Safari 11+ - (Mobile) Safari 12+
Not all of these were tested though, compatibility reports are very welcome! Not all of these versions are tested on regular basis though, compatibility reports are very welcome!
## Getting started ## Getting started
@ -138,7 +209,7 @@ app by following these steps:
brew install swiftwasm/tap/carton brew install swiftwasm/tap/carton
``` ```
If you had `carton` installed before this, make sure you have version 0.9.0 or greater: If you had `carton` installed before this, make sure you have version 0.15.0 or greater:
``` ```
carton --version carton --version
@ -170,6 +241,26 @@ carton dev
You can also clone this repository and run `carton dev --product TokamakDemo` in its root You can also clone this repository and run `carton dev --product TokamakDemo` in its root
directory. This will build the demo app that shows almost all of the currently implemented APIs. directory. This will build the demo app that shows almost all of the currently implemented APIs.
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
## Security
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
to override this functionality, you can use the `_domTextSanitizer` modifier:
```swift
Text("<font color='red'>Unsanitized Text</font>")
._domTextSanitizer(Sanitizers.HTML.insecure)
```
You can also use custom sanitizers; the argument to `_domTextSanitizer` is simply a
`String -> String` closure. If `_domTextSanitizer` is applied to a non-`Text` view,
it will apply to all `Text` in subviews, unless overridden.
If you use user-generated or otherwise unsafe strings elsewhere, make sure to properly
sanitize them yourself.
## Troubleshooting ## Troubleshooting
### `unable to find utility "xctest"` error when building ### `unable to find utility "xctest"` error when building
@ -205,63 +296,14 @@ doesn't provide an official build of the extension on the VSCode Marketplace unf
## Contributing ## Contributing
### Modular structure All contributions, no matter how small, are very welcome. You don't have to be a web developer or a
SwiftUI expert to meaningfully contribute. In fact, by checking out how some of the simplest views are
implemented in Tokamak you may learn more how SwiftUI may work under the hood.
Tokamak is built with modularity in mind, providing a multi-platform `TokamakCore` module and Updating our [documentation](https://github.com/TokamakUI/Tokamak/tree/main/docs) and taking on [the starter
separate modules for platform-specific renderers. Currently, the only available renderer modules are bugs](https://github.com/TokamakUI/Tokamak/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side is also appreciated. Don't forget to join our [Discord server](https://discord.gg/ashJW8T8yp) to get in
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers touch with the maintainers and other users. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.
guide](docs/RenderersGuide.md) for more details.
Tokamak users only need to import a renderer module they would like to use, while
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
those symbols with underscores in their names to indicate this. It can be formulated as these
"rules":
1. If a symbol is restricted to a module and has no `public` access control, no need for an
underscore.
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
renderer module via `public typealias`.
3. If a function or a type have `public` on them only by necessity to make them available in
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
indicate that.
The benefit of separate modules is that they allow us to provide separate renderers for different
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
`TokamakAndroid` with no need to be aware of any of the web modules.
### Coding Style
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
[SwiftLint](https://github.com/realm/SwiftLint) to enforce formatting and coding style. SwiftFormat
0.45.3 and SwiftLint 0.39.2 or later versions are recommended. We encourage you to run SwiftFormat
and SwiftLint within a local clone of the repository in whatever way works best for you. You can do
that either manually, or automatically with VSCode extensions for
[SwiftFormat](https://github.com/vknabel/vscode-swiftformat) and
[SwiftLint](https://github.com/vknabel/vscode-swiftlint) respectively, or with the [Xcode
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension), or [build
phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase).
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run
this once to set up the [pre-commit](https://pre-commit.com/) hook:
```
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit
```
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
and installation instructions for other platforms.
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
fail with inconsistent formatting or style. We require CI builds to pass for all
PRs before merging.
### Code of Conduct ### Code of Conduct
@ -283,9 +325,10 @@ appreciated and helps in maintaining the project.
## Maintainers ## Maintainers
In alphabetical order: [Carson Katri](https://github.com/carson-katri), In alphabetical order: [Carson Katri](https://github.com/carson-katri),
[David Hunt](https://github.com/foscomputerservices), [Ezra Berch](https://github.com/ezraberch),
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com), [Jed Fox](https://jedfox.com),
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/). [Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
[Yuta Saito](https://github.com/kateinoigakukun/).
## Acknowledgments ## Acknowledgments

View File

@ -0,0 +1,165 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
import Foundation
public protocol Animatable {
associatedtype AnimatableData: VectorArithmetic
var animatableData: Self.AnimatableData { get set }
}
public protocol _PrimitiveAnimatable {}
public extension Animatable where Self: VectorArithmetic {
var animatableData: Self {
get { self }
// swiftlint:disable:next unused_setter_value
set {}
}
}
public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
var animatableData: EmptyAnimatableData {
@inlinable get { EmptyAnimatableData() }
// swiftlint:disable:next unused_setter_value
@inlinable set {}
}
}
@frozen
public struct EmptyAnimatableData: VectorArithmetic {
@inlinable
public init() {}
@inlinable
public static var zero: Self { .init() }
@inlinable
public static func += (lhs: inout Self, rhs: Self) {}
@inlinable
public static func -= (lhs: inout Self, rhs: Self) {}
@inlinable
public static func + (lhs: Self, rhs: Self) -> Self {
.zero
}
@inlinable
public static func - (lhs: Self, rhs: Self) -> Self {
.zero
}
@inlinable
public mutating func scale(by rhs: Double) {}
@inlinable
public var magnitudeSquared: Double { .zero }
public static func == (a: Self, b: Self) -> Bool { true }
}
@frozen
public struct AnimatablePair<First, Second>: VectorArithmetic
where First: VectorArithmetic, Second: VectorArithmetic
{
public var first: First
public var second: Second
@inlinable
public init(_ first: First, _ second: Second) {
self.first = first
self.second = second
}
@inlinable
internal subscript() -> (First, Second) {
get { (first, second) }
set { (first, second) = newValue }
}
@_transparent
public static var zero: Self {
@_transparent get {
.init(First.zero, Second.zero)
}
}
@_transparent
public static func += (lhs: inout Self, rhs: Self) {
lhs.first += rhs.first
lhs.second += rhs.second
}
@_transparent
public static func -= (lhs: inout Self, rhs: Self) {
lhs.first -= rhs.first
lhs.second -= rhs.second
}
@_transparent
public static func + (lhs: Self, rhs: Self) -> Self {
.init(lhs.first + rhs.first, lhs.second + rhs.second)
}
@_transparent
public static func - (lhs: Self, rhs: Self) -> Self {
.init(lhs.first - rhs.first, lhs.second - rhs.second)
}
@_transparent
public mutating func scale(by rhs: Double) {
first.scale(by: rhs)
second.scale(by: rhs)
}
@_transparent
public var magnitudeSquared: Double {
@_transparent get {
first.magnitudeSquared + second.magnitudeSquared
}
}
public static func == (a: Self, b: Self) -> Bool {
a.first == b.first
&& a.second == b.second
}
}
extension CGPoint: Animatable {
public var animatableData: AnimatablePair<CGFloat, CGFloat> {
@inlinable get { .init(x, y) }
@inlinable set { (x, y) = newValue[] }
}
}
extension CGSize: Animatable {
public var animatableData: AnimatablePair<CGFloat, CGFloat> {
@inlinable get { .init(width, height) }
@inlinable set { (width, height) = newValue[] }
}
}
extension CGRect: Animatable {
public var animatableData: AnimatablePair<CGPoint.AnimatableData, CGSize.AnimatableData> {
@inlinable get {
.init(origin.animatableData, size.animatableData)
}
@inlinable set {
(origin.animatableData, size.animatableData) = newValue[]
}
}
}

View File

@ -11,9 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
#if canImport(Combine) public protocol AnimatableModifier: Animatable, ViewModifier {}
@_exported import Combine
#else
@_exported import OpenCombine
#endif

View File

@ -0,0 +1,220 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// This default is specified in SwiftUI on `Animation.timingCurve` as `0.35`.
public let defaultDuration = 0.35
public struct Animation: Equatable {
fileprivate var box: _AnimationBoxBase
private init(_ box: _AnimationBoxBase) {
self.box = box
}
public static let `default` = Self.easeInOut
public func delay(_ delay: Double) -> Animation {
.init(DelayedAnimationBox(delay: delay, parent: box))
}
public func speed(_ speed: Double) -> Animation {
.init(RetimedAnimationBox(speed: speed, parent: box))
}
public func repeatCount(
_ repeatCount: Int,
autoreverses: Bool = true
) -> Animation {
.init(RepeatedAnimationBox(style: .fixed(repeatCount, autoreverses: autoreverses), parent: box))
}
public func repeatForever(autoreverses: Bool = true) -> Animation {
.init(RepeatedAnimationBox(style: .forever(autoreverses: autoreverses), parent: box))
}
public static func spring(
response: Double = 0.55,
dampingFraction: Double = 0.825,
blendDuration: Double = 0
) -> Animation {
if response == 0 { // Infinitely stiff spring
// (well, not .infinity, but a very high number)
return interpolatingSpring(stiffness: 999, damping: 999)
} else {
return interpolatingSpring(
mass: 1,
stiffness: pow(2 * .pi / response, 2),
damping: 4 * .pi * dampingFraction / response
)
}
}
public static func interactiveSpring(
response: Double = 0.15,
dampingFraction: Double = 0.86,
blendDuration: Double = 0.25
) -> Animation {
spring(
response: response,
dampingFraction: dampingFraction,
blendDuration: blendDuration
)
}
public static func interpolatingSpring(
mass: Double = 1.0,
stiffness: Double,
damping: Double,
initialVelocity: Double = 0.0
) -> Animation {
.init(StyleAnimationBox(style: .solver(_AnimationSolvers.Spring(
mass: mass,
stiffness: stiffness,
damping: damping,
initialVelocity: initialVelocity
))))
}
public static func easeInOut(duration: Double) -> Animation {
timingCurve(0.42, 0, 0.58, 1.0, duration: duration)
}
public static var easeInOut: Animation {
easeInOut(duration: defaultDuration)
}
public static func easeIn(duration: Double) -> Animation {
timingCurve(0.42, 0, 1.0, 1.0, duration: duration)
}
public static var easeIn: Animation {
easeIn(duration: defaultDuration)
}
public static func easeOut(duration: Double) -> Animation {
timingCurve(0, 0, 0.58, 1.0, duration: duration)
}
public static var easeOut: Animation {
easeOut(duration: defaultDuration)
}
public static func linear(duration: Double) -> Animation {
timingCurve(0, 0, 1, 1, duration: duration)
}
public static var linear: Animation {
timingCurve(0, 0, 1, 1)
}
public static func timingCurve(
_ c0x: Double,
_ c0y: Double,
_ c1x: Double,
_ c1y: Double,
duration: Double = defaultDuration
) -> Animation {
.init(StyleAnimationBox(style: .timingCurve(c0x, c0y, c1x, c1y, duration: duration)))
}
}
public struct _AnimationProxy {
let subject: Animation
public init(_ subject: Animation) { self.subject = subject }
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
}
@frozen
public struct _AnimationModifier<Value>: ViewModifier, Equatable
where Value: Equatable
{
public var animation: Animation?
public var value: Value
@inlinable
public init(animation: Animation?, value: Value) {
self.animation = animation
self.value = value
}
private struct ContentWrapper: View, Equatable {
let content: Content
let animation: Animation?
let value: Value
@State
private var lastValue: Value?
var body: some View {
content.transaction {
if lastValue != value {
$0.animation = animation
}
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.value == rhs.value
}
}
public func body(content: Content) -> some View {
ContentWrapper(content: content, animation: animation, value: value)
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.value == rhs.value
&& lhs.animation == rhs.animation
}
}
@frozen
public struct _AnimationView<Content>: View
where Content: Equatable, Content: View
{
public var content: Content
public var animation: Animation?
@inlinable
public init(content: Content, animation: Animation?) {
self.content = content
self.animation = animation
}
public var body: some View {
content
.modifier(_AnimationModifier(animation: animation, value: content))
}
}
public extension View {
@inlinable
func animation<V>(
_ animation: Animation?,
value: V
) -> some View where V: Equatable {
modifier(_AnimationModifier(animation: animation, value: value))
}
}
public extension View where Self: Equatable {
@inlinable
func animation(_ animation: Animation?) -> some View {
_AnimationView(content: self, animation: animation)
}
}

View File

@ -0,0 +1,127 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
public struct Transaction {
/// The overridden transaction for a state change in a `withTransaction` block.
/// Is always set back to `nil` when the block exits.
static var _active: Self?
public var animation: Animation?
/** `true` in the first part of the transition update, this avoids situations when `animation(_:)`
could add more animations to this transaction.
*/
public var disablesAnimations: Bool
public init(animation: Animation?) {
self.animation = animation
disablesAnimations = false
}
}
public func withTransaction<Result>(
_ transaction: Transaction,
_ body: () throws -> Result
) rethrows -> Result {
Transaction._active = transaction
defer { Transaction._active = nil }
return try body()
}
public func withAnimation<Result>(
_ animation: Animation? = .default,
_ body: () throws -> Result
) rethrows -> Result {
try withTransaction(.init(animation: animation), body)
}
protocol _TransactionModifierProtocol {
func modifyTransaction(_ transaction: inout Transaction)
}
@frozen
public struct _TransactionModifier: ViewModifier {
public var transform: (inout Transaction) -> ()
@inlinable
public init(transform: @escaping (inout Transaction) -> ()) {
self.transform = transform
}
public func body(content: Content) -> some View {
content
}
}
extension _TransactionModifier: _TransactionModifierProtocol {
func modifyTransaction(_ transaction: inout Transaction) {
transform(&transaction)
}
}
extension ModifiedContent: _TransactionModifierProtocol
where Modifier: _TransactionModifierProtocol
{
func modifyTransaction(_ transaction: inout Transaction) {
modifier.modifyTransaction(&transaction)
}
}
@frozen
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
public var content: V
public var base: _TransactionModifier
@inlinable
public init(
content: V,
transform: @escaping (inout Transaction) -> ()
) {
self.content = content
base = .init(transform: transform)
}
public func body(content: Content) -> some View {
content
.modifier(self.content)
.modifier(base)
}
}
public extension View {
@inlinable
func transaction(_ transform: @escaping (inout Transaction) -> ()) -> some View {
modifier(_TransactionModifier(transform: transform))
}
}
public extension ViewModifier {
@inlinable
func transaction(
_ transform: @escaping (inout Transaction) -> ()
) -> some ViewModifier {
_PushPopTransactionModifier(content: self, transform: transform)
}
@inlinable
func animation(
_ animation: Animation?
) -> some ViewModifier {
transaction { t in
if !t.disablesAnimations {
t.animation = animation
}
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
import Foundation
public protocol VectorArithmetic: AdditiveArithmetic {
mutating func scale(by rhs: Double)
var magnitudeSquared: Double { get }
}
extension Float: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) }
}
}
extension Double: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= rhs }
@_transparent
public var magnitudeSquared: Double {
@_transparent get { self * self }
}
}
extension CGFloat: VectorArithmetic {
@_transparent
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
@_transparent
public var magnitudeSquared: Double {
@_transparent get { Double(self * self) }
}
}

View File

@ -0,0 +1,164 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
import Foundation
public class _AnimationBoxBase: Equatable {
public struct _Resolved {
public var duration: Double {
switch style {
case let .timingCurve(_, _, _, _, duration):
return duration
case let .solver(solver):
return solver.restingPoint(precision: 0.01)
}
}
public var delay: Double
public var speed: Double
public var repeatStyle: _RepeatStyle
public var style: _Style
public enum _Style: Equatable {
case timingCurve(Double, Double, Double, Double, duration: Double)
case solver(_AnimationSolver)
public static func == (lhs: Self, rhs: Self) -> Bool {
switch lhs {
case let .timingCurve(lhs0, lhs1, lhs2, lhs3, lhsDuration):
if case let .timingCurve(rhs0, rhs1, rhs2, rhs3, rhsDuration) = rhs {
return lhs0 == rhs0
&& lhs1 == rhs1
&& lhs2 == rhs2
&& lhs3 == rhs3
&& lhsDuration == rhsDuration
}
case let .solver(lhsSolver):
if case let .solver(rhsSolver) = rhs {
return type(of: lhsSolver) == type(of: rhsSolver)
}
}
return false
}
}
public enum _RepeatStyle: Equatable {
case fixed(Int, autoreverses: Bool)
case forever(autoreverses: Bool)
public var autoreverses: Bool {
switch self {
case let .fixed(_, autoreverses),
let .forever(autoreverses):
return autoreverses
}
}
}
}
func resolve() -> _Resolved {
fatalError("implement \(#function) in subclass")
}
func equals(_ other: _AnimationBoxBase) -> Bool {
fatalError("implement \(#function) in subclass")
}
public static func == (lhs: _AnimationBoxBase, rhs: _AnimationBoxBase) -> Bool {
lhs.equals(rhs)
}
}
final class StyleAnimationBox: _AnimationBoxBase {
let style: _Resolved._Style
init(style: _Resolved._Style) {
self.style = style
}
override func resolve() -> _AnimationBoxBase._Resolved {
.init(delay: 0, speed: 1, repeatStyle: .fixed(1, autoreverses: true), style: style)
}
override func equals(_ other: _AnimationBoxBase) -> Bool {
guard let other = other as? StyleAnimationBox else { return false }
return style == other.style
}
}
final class DelayedAnimationBox: _AnimationBoxBase {
let delay: Double
let parent: _AnimationBoxBase
init(delay: Double, parent: _AnimationBoxBase) {
self.delay = delay
self.parent = parent
}
override func resolve() -> _AnimationBoxBase._Resolved {
var resolved = parent.resolve()
resolved.delay = delay
return resolved
}
override func equals(_ other: _AnimationBoxBase) -> Bool {
guard let other = other as? DelayedAnimationBox else { return false }
return delay == other.delay && parent.equals(other.parent)
}
}
final class RetimedAnimationBox: _AnimationBoxBase {
let speed: Double
let parent: _AnimationBoxBase
init(speed: Double, parent: _AnimationBoxBase) {
self.speed = speed
self.parent = parent
}
override func resolve() -> _AnimationBoxBase._Resolved {
var resolved = parent.resolve()
resolved.speed = speed
return resolved
}
override func equals(_ other: _AnimationBoxBase) -> Bool {
guard let other = other as? RetimedAnimationBox else { return false }
return speed == other.speed && parent.equals(other.parent)
}
}
final class RepeatedAnimationBox: _AnimationBoxBase {
let style: _AnimationBoxBase._Resolved._RepeatStyle
let parent: _AnimationBoxBase
init(style: _AnimationBoxBase._Resolved._RepeatStyle, parent: _AnimationBoxBase) {
self.style = style
self.parent = parent
}
override func resolve() -> _AnimationBoxBase._Resolved {
var resolved = parent.resolve()
resolved.repeatStyle = style
return resolved
}
override func equals(_ other: _AnimationBoxBase) -> Bool {
guard let other = other as? RepeatedAnimationBox else { return false }
return style == other.style && parent.equals(other.parent)
}
}

View File

@ -0,0 +1,66 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
import Foundation
/// A solver for an animation with a duration that depends on its properties.
public protocol _AnimationSolver {
/// Solve value at a specific point in time.
func solve(at t: Double) -> Double
/// Calculates the duration of the animation to a specific precision.
func restingPoint(precision y: Double) -> Double
}
public enum _AnimationSolvers {
// swiftlint:disable line_length
/// Calculates the animation of a spring with certain properties.
///
/// For some useful information, see
/// [Demystifying UIKit Spring Animations](https://medium.com/ios-os-x-development/demystifying-uikit-spring-animations-2bb868446773)
public struct Spring: _AnimationSolver {
// swiftlint:enable line_length
let ƛ: Double
let w0: Double
let wd: Double
/// Initial velocity
let v0: Double
/// Target value
let s0: Double = 1
public init(mass: Double, stiffness: Double, damping: Double, initialVelocity: Double) {
ƛ = (damping * 0.755) / (mass * 2)
w0 = sqrt(stiffness / 2)
wd = sqrt(abs(pow(w0, 2) - pow(ƛ, 2)))
v0 = initialVelocity
}
public func solve(at t: Double) -> Double {
let y: Double
if ƛ < w0 {
y = pow(M_E, -(ƛ * t)) * ((s0 * cos(wd * t)) + ((v0 + s0) * sin(wd * t)))
// } else if ƛ > w0 { // Overdamping is unsupported on Apple platforms
} else {
y = pow(M_E, -(ƛ * t)) * (s0 + ((v0 + (ƛ * s0)) * t))
}
return 1 - y
}
public func restingPoint(precision y: Double) -> Double {
log(y) / -ƛ
}
}
}

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

@ -0,0 +1,87 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
import Foundation
public protocol _VectorMath: Animatable {}
public extension _VectorMath {
@inlinable
var magnitude: Double {
animatableData.magnitudeSquared.squareRoot()
}
@inlinable
mutating func negate() {
animatableData = .zero - animatableData
}
@inlinable
static prefix func - (operand: Self) -> Self {
var result = operand
result.negate()
return result
}
@inlinable
static func += (lhs: inout Self, rhs: Self) {
lhs.animatableData += rhs.animatableData
}
@inlinable
static func + (lhs: Self, rhs: Self) -> Self {
var result = lhs
result += rhs
return result
}
@inlinable
static func -= (lhs: inout Self, rhs: Self) {
lhs.animatableData -= rhs.animatableData
}
@inlinable
static func - (lhs: Self, rhs: Self) -> Self {
var result = lhs
result -= rhs
return result
}
@inlinable
static func *= (lhs: inout Self, rhs: Double) {
lhs.animatableData.scale(by: rhs)
}
@inlinable
static func * (lhs: Self, rhs: Double) -> Self {
var result = lhs
result *= rhs
return result
}
@inlinable
static func /= (lhs: inout Self, rhs: Double) {
lhs *= 1 / rhs
}
@inlinable
static func / (lhs: Self, rhs: Double) -> Self {
var result = lhs
result /= rhs
return result
}
}

View File

@ -15,7 +15,7 @@
// Created by Carson Katri on 7/16/20. // Created by Carson Katri on 7/16/20.
// //
import CombineShim import OpenCombineShim
/// Provides the ability to set the title of the Scene. /// Provides the ability to set the title of the Scene.
public protocol _TitledApp { public protocol _TitledApp {
@ -28,7 +28,10 @@ public protocol App: _TitledApp {
var body: Body { get } var body: Body { get }
/// Implemented by the renderer to mount the `App` /// Implemented by the renderer to mount the `App`
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) static func _launch(
_ app: Self,
with configuration: _AppConfiguration
)
/// Implemented by the renderer to update the `App` on `ScenePhase` changes /// Implemented by the renderer to update the `App` on `ScenePhase` changes
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get } var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
@ -36,14 +39,38 @@ public protocol App: _TitledApp {
/// Implemented by the renderer to update the `App` on `ColorScheme` changes /// Implemented by the renderer to update the `App` on `ColorScheme` changes
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get } var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
static var _configuration: _AppConfiguration { get }
static func main() static func main()
init() init()
} }
public extension App { public struct _AppConfiguration {
static func main() { public let reconciler: Reconciler
let app = Self() public let rootEnvironment: EnvironmentValues
_launch(app, EnvironmentValues())
public init(
reconciler: Reconciler = .stack,
rootEnvironment: EnvironmentValues = .init()
) {
self.reconciler = reconciler
self.rootEnvironment = rootEnvironment
}
public enum Reconciler {
/// Use the `StackReconciler`.
case stack
/// Use the `FiberReconciler` with layout steps optionally enabled.
case fiber(useDynamicLayout: Bool = false)
}
}
public extension App {
static var _configuration: _AppConfiguration { .init() }
static func main() {
let app = Self()
_launch(app, with: Self._configuration)
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,11 +15,15 @@
// Created by Carson Katri on 7/16/20. // Created by Carson Katri on 7/16/20.
// //
import CombineShim import OpenCombineShim
@propertyWrapper public struct AppStorage<Value>: DynamicProperty { @propertyWrapper
public struct AppStorage<Value>: DynamicProperty {
let provider: _StorageProvider? let provider: _StorageProvider?
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
@Environment(\._defaultAppStorage)
var defaultProvider: _StorageProvider?
var unwrappedProvider: _StorageProvider { var unwrappedProvider: _StorageProvider {
provider ?? defaultProvider! provider ?? defaultProvider!
} }
@ -173,6 +177,7 @@ struct DefaultAppStorageEnvironmentKey: EnvironmentKey {
} }
public extension EnvironmentValues { public extension EnvironmentValues {
@_spi(TokamakCore)
var _defaultAppStorage: _StorageProvider? { var _defaultAppStorage: _StorageProvider? {
get { get {
self[DefaultAppStorageEnvironmentKey.self] self[DefaultAppStorageEnvironmentKey.self]

View File

@ -21,8 +21,23 @@ public protocol Scene {
// FIXME: If I put `@SceneBuilder` in front of this // FIXME: If I put `@SceneBuilder` in front of this
// it fails to build with no useful error message. // it fails to build with no useful error message.
var body: Self.Body { get } var body: Self.Body { get }
/// Override the default implementation for `Scene`s with body types of `Never`
/// or in cases where the body would normally need to be type erased.
///
/// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor`
func _visitChildren<V: SceneVisitor>(_ visitor: V)
/// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom
/// `LayoutComputer` from the `SceneInputs`.
///
/// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`.
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs
} }
public typealias SceneInputs<S: Scene> = ViewInputs<S>
public typealias SceneOutputs = ViewOutputs
protocol TitledScene { protocol TitledScene {
var title: Text? { get } var title: Text? { get }
} }

View File

@ -15,7 +15,7 @@
// Created by Carson Katri on 7/16/20. // Created by Carson Katri on 7/16/20.
// //
@_functionBuilder @resultBuilder
public enum SceneBuilder { public enum SceneBuilder {
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene { public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
content content
@ -29,7 +29,14 @@ public extension SceneBuilder {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene, static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
C1: Scene C1: Scene
{ {
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)]) _TupleScene(
(c0, c1),
children: [_AnyScene(c0), _AnyScene(c1)],
visit: {
$0.visit(c0)
$0.visit(c1)
}
)
} }
} }
@ -37,7 +44,15 @@ public extension SceneBuilder {
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
where C0: Scene, C1: Scene, C2: Scene where C0: Scene, C1: Scene, C2: Scene
{ {
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)]) _TupleScene(
(c0, c1, c2),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
}
)
} }
} }
@ -50,7 +65,13 @@ public extension SceneBuilder {
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
_TupleScene( _TupleScene(
(c0, c1, c2, c3), (c0, c1, c2, c3),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)] children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
}
) )
} }
} }
@ -65,7 +86,14 @@ public extension SceneBuilder {
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene { ) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
_TupleScene( _TupleScene(
(c0, c1, c2, c3, c4), (c0, c1, c2, c3, c4),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)] children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
}
) )
} }
} }
@ -90,7 +118,15 @@ public extension SceneBuilder {
_AnyScene(c3), _AnyScene(c3),
_AnyScene(c4), _AnyScene(c4),
_AnyScene(c5), _AnyScene(c5),
] ],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
}
) )
} }
} }
@ -117,7 +153,16 @@ public extension SceneBuilder {
_AnyScene(c4), _AnyScene(c4),
_AnyScene(c5), _AnyScene(c5),
_AnyScene(c6), _AnyScene(c6),
] ],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
}
) )
} }
} }
@ -146,7 +191,17 @@ public extension SceneBuilder {
_AnyScene(c5), _AnyScene(c5),
_AnyScene(c6), _AnyScene(c6),
_AnyScene(c7), _AnyScene(c7),
] ],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
}
) )
} }
} }
@ -177,7 +232,18 @@ public extension SceneBuilder {
_AnyScene(c6), _AnyScene(c6),
_AnyScene(c7), _AnyScene(c7),
_AnyScene(c8), _AnyScene(c8),
] ],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
}
) )
} }
} }
@ -210,7 +276,19 @@ public extension SceneBuilder {
_AnyScene(c7), _AnyScene(c7),
_AnyScene(c8), _AnyScene(c8),
_AnyScene(c9), _AnyScene(c9),
] ],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
$0.visit(c9)
}
) )
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/17/20. // Created by Carson Katri on 7/17/20.
// //
import CombineShim import OpenCombineShim
/// The renderer must specify a default `_StorageProvider` before any `SceneStorage` /// The renderer must specify a default `_StorageProvider` before any `SceneStorage`
/// values are accessed. /// values are accessed.
@ -23,7 +23,8 @@ public enum _DefaultSceneStorageProvider {
public static var `default`: _StorageProvider! public static var `default`: _StorageProvider!
} }
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty { @propertyWrapper
public struct SceneStorage<Value>: DynamicProperty {
let key: String let key: String
let defaultValue: Value let defaultValue: Value
let store: (_StorageProvider, String, Value) -> () let store: (_StorageProvider, String, Value) -> ()

View File

@ -63,6 +63,7 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
self.content = content() self.content = content()
} }
@_spi(TokamakCore)
public var body: Never { public var body: Never {
neverScene("WindowGroup") neverScene("WindowGroup")
} }
@ -74,4 +75,8 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
// public init(_ titleKey: LocalizedStringKey, // public init(_ titleKey: LocalizedStringKey,
// @ViewBuilder content: () -> Content) { // @ViewBuilder content: () -> Content) {
// } // }
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
visitor.visit(content)
}
} }

View File

@ -25,6 +25,7 @@ public struct _SceneModifier_Content<Modifier>: Scene where Modifier: _SceneModi
public let modifier: Modifier public let modifier: Modifier
public let scene: _AnyScene public let scene: _AnyScene
@_spi(TokamakCore)
public var body: Never { public var body: Never {
neverScene("_SceneModifier_Content") neverScene("_SceneModifier_Content")
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/19/20. // Created by Carson Katri on 7/19/20.
// //
import CombineShim import OpenCombineShim
public struct _AnyApp: App { public struct _AnyApp: App {
var app: Any var app: Any
@ -31,26 +31,36 @@ public struct _AnyApp: App {
bodyType = A.Body.self bodyType = A.Body.self
} }
@_spi(TokamakCore)
public var body: Never { public var body: Never {
neverScene("_AnyApp") neverScene("_AnyApp")
} }
@_spi(TokamakCore)
public init() { public init() {
fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.") fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.")
} }
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) { @_spi(TokamakCore)
public static func _launch(_ app: Self, with configuration: _AppConfiguration) {
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.") fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
} }
@_spi(TokamakCore)
public static func _setTitle(_ title: String) { public static func _setTitle(_ title: String) {
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.") fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
} }
public static var _configuration: _AppConfiguration {
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
}
@_spi(TokamakCore)
public var _phasePublisher: AnyPublisher<ScenePhase, Never> { public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.") fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
} }
@_spi(TokamakCore)
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.") fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
} }

View File

@ -66,6 +66,7 @@ public struct _AnyScene: Scene {
} }
} }
@_spi(TokamakCore)
public var body: Never { public var body: Never {
neverScene("_AnyScene") neverScene("_AnyScene")
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/22/20. // Created by Carson Katri on 7/22/20.
// //
import CombineShim import OpenCombineShim
public protocol _StorageProvider { public protocol _StorageProvider {
func store(key: String, value: Bool?) func store(key: String, value: Bool?)

View File

@ -17,11 +17,17 @@
struct _TupleScene<T>: Scene, GroupScene { struct _TupleScene<T>: Scene, GroupScene {
let value: T let value: T
var children: [_AnyScene] let children: [_AnyScene]
let visit: (SceneVisitor) -> ()
init(_ value: T, children: [_AnyScene]) { init(
_ value: T,
children: [_AnyScene],
visit: @escaping (SceneVisitor) -> ()
) {
self.value = value self.value = value
self.children = children self.children = children
self.visit = visit
} }
var body: Never { var body: Never {

View File

@ -23,7 +23,8 @@ protocol EnvironmentReader {
mutating func setContent(from values: EnvironmentValues) mutating func setContent(from values: EnvironmentValues)
} }
@propertyWrapper public struct Environment<Value>: DynamicProperty { @propertyWrapper
public struct Environment<Value>: DynamicProperty {
enum Content { enum Content {
case keyPath(KeyPath<EnvironmentValues, Value>) case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value) case value(Value)

View File

@ -17,11 +17,24 @@ public protocol EnvironmentKey {
static var defaultValue: Value { get } static var defaultValue: Value { get }
} }
protocol EnvironmentModifier { /// This protocol defines a type which mutates the environment in some way.
/// Unlike `EnvironmentalModifier`, which reads the environment to
/// create a `ViewModifier`.
///
/// It can be applied to a `View` or `ViewModifier`.
public protocol _EnvironmentModifier {
func modifyEnvironment(_ values: inout EnvironmentValues) func modifyEnvironment(_ values: inout EnvironmentValues)
} }
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier { public extension ViewModifier where Self: _EnvironmentModifier {
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
var environment = inputs.environment.environment
inputs.content.modifyEnvironment(&environment)
return .init(inputs: inputs, environment: environment)
}
}
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentModifier {
public let keyPath: WritableKeyPath<EnvironmentValues, Value> public let keyPath: WritableKeyPath<EnvironmentValues, Value>
public let value: Value public let value: Value
@ -32,7 +45,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo
public typealias Body = Never public typealias Body = Never
func modifyEnvironment(_ values: inout EnvironmentValues) { public func modifyEnvironment(_ values: inout EnvironmentValues) {
values[keyPath: keyPath] = value values[keyPath: keyPath] = value
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,12 +15,14 @@
// Created by Carson Katri on 7/7/20. // Created by Carson Katri on 7/7/20.
// //
import CombineShim import OpenCombineShim
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty @propertyWrapper
public struct EnvironmentObject<ObjectType>: DynamicProperty
where ObjectType: ObservableObject where ObjectType: ObservableObject
{ {
@dynamicMemberLookup public struct Wrapper { @dynamicMemberLookup
public struct Wrapper {
internal let root: ObjectType internal let root: ObjectType
public subscript<Subject>( public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject> dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import CombineShim import OpenCombineShim
public struct EnvironmentValues: CustomStringConvertible { public struct EnvironmentValues: CustomStringConvertible {
public var description: String { public var description: String {
@ -43,6 +43,22 @@ public struct EnvironmentValues: CustomStringConvertible {
values[bindable] = newValue values[bindable] = newValue
} }
} }
@_spi(TokamakCore)
public mutating func merge(_ other: Self?) {
if let other = other {
values.merge(other.values) { _, new in
new
}
}
}
@_spi(TokamakCore)
public func merging(_ other: Self?) -> Self {
var merged = self
merged.merge(other)
return merged
}
} }
struct IsEnabledKey: EnvironmentKey { struct IsEnabledKey: EnvironmentKey {
@ -60,7 +76,7 @@ public extension EnvironmentValues {
} }
} }
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier { struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
let environmentValues: EnvironmentValues let environmentValues: EnvironmentValues
func body(content: Content) -> some View { func body(content: Content) -> some View {

View File

@ -0,0 +1,46 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
/// A modifier that resolves to a concrete modifier in an environment.
public protocol EnvironmentalModifier: ViewModifier {
associatedtype ResolvedModifier: ViewModifier
func resolve(in environment: EnvironmentValues) -> ResolvedModifier
static var _requiresMainThread: Bool { get }
}
private struct EnvironmentalModifierResolver<M>: ViewModifier, EnvironmentReader
where M: EnvironmentalModifier
{
let modifier: M
var resolved: M.ResolvedModifier!
func body(content: Content) -> some View {
content.modifier(resolved)
}
mutating func setContent(from values: EnvironmentValues) {
resolved = modifier.resolve(in: values)
}
}
public extension EnvironmentalModifier {
static var _requiresMainThread: Bool { true }
func body(content: _ViewModifier_Content<Self>) -> some View {
content.modifier(EnvironmentalModifierResolver(modifier: self))
}
}

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

@ -0,0 +1,72 @@
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
@frozen
public enum ContentMode: Hashable, CaseIterable {
case fit
case fill
}
public struct _AspectRatioLayout: ViewModifier {
public let aspectRatio: CGFloat?
public let contentMode: ContentMode
@inlinable
public init(aspectRatio: CGFloat?, contentMode: ContentMode) {
self.aspectRatio = aspectRatio
self.contentMode = contentMode
}
public func body(content: Content) -> some View {
content
}
}
public extension View {
@inlinable
func aspectRatio(
_ aspectRatio: CGFloat? = nil,
contentMode: ContentMode
) -> some View {
modifier(
_AspectRatioLayout(
aspectRatio: aspectRatio,
contentMode: contentMode
)
)
}
@inlinable
func aspectRatio(
_ aspectRatio: CGSize,
contentMode: ContentMode
) -> some View {
self.aspectRatio(
aspectRatio.width / aspectRatio.height,
contentMode: contentMode
)
}
@inlinable
func scaledToFit() -> some View {
aspectRatio(contentMode: .fit)
}
@inlinable
func scaledToFill() -> some View {
aspectRatio(contentMode: .fill)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
// Created by Carson Katri on 06/29/2020. // Created by Carson Katri on 06/29/2020.
// //
import Foundation
public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape { public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
public var shape: ClipShape public var shape: ClipShape
public var style: FillStyle public var style: FillStyle
@ -27,6 +29,11 @@ public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
public func body(content: Content) -> some View { public func body(content: Content) -> some View {
content content
} }
public var animatableData: ClipShape.AnimatableData {
get { shape.animatableData }
set { shape.animatableData = newValue }
}
} }
public extension View { public extension View {

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,8 +15,10 @@
// Created by Carson Katri on 7/3/20. // Created by Carson Katri on 7/3/20.
// //
import Foundation
// FIXME: Make `Animatable` // FIXME: Make `Animatable`
public protocol GeometryEffect: ViewModifier { public protocol GeometryEffect: Animatable, ViewModifier {
func effectValue(size: CGSize) -> ProjectionTransform func effectValue(size: CGSize) -> ProjectionTransform
} }

View File

@ -0,0 +1,57 @@
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/12/21.
//
import Foundation
@frozen
public struct _OffsetEffect: GeometryEffect, Equatable {
public var offset: CGSize
@inlinable
public init(offset: CGSize) {
self.offset = offset
}
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(.init(translationX: offset.width, y: offset.height))
}
public var animatableData: CGSize.AnimatableData {
get {
offset.animatableData
}
set {
offset.animatableData = newValue
}
}
public func body(content: Content) -> some View {
content
}
}
public extension View {
@inlinable
func offset(_ offset: CGSize) -> some View {
modifier(_OffsetEffect(offset: offset))
}
@inlinable
func offset(x: CGFloat = 0, y: CGFloat = 0) -> some View {
offset(CGSize(width: x, height: y))
}
}

View File

@ -0,0 +1,39 @@
// Copyright 2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 1/20/21.
//
public struct _OpacityEffect: Animatable, ViewModifier, Equatable {
public var opacity: Double
public init(opacity: Double) {
self.opacity = opacity
}
public func body(content: Content) -> some View {
content
}
public var animatableData: Double {
get { opacity }
set { opacity = newValue }
}
}
public extension View {
func opacity(_ opacity: Double) -> some View {
modifier(_OpacityEffect(opacity: opacity))
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
// Created by Carson Katri on 7/3/20. // Created by Carson Katri on 7/3/20.
// //
import Foundation
public struct _RotationEffect: GeometryEffect { public struct _RotationEffect: GeometryEffect {
public var angle: Angle public var angle: Angle
public var anchor: UnitPoint public var anchor: UnitPoint
@ -25,12 +27,21 @@ public struct _RotationEffect: GeometryEffect {
} }
public func effectValue(size: CGSize) -> ProjectionTransform { public func effectValue(size: CGSize) -> ProjectionTransform {
.init(CGAffineTransform.identity.rotated(by: angle.radians)) .init(CGAffineTransform.identity.rotated(by: CGFloat(angle.radians)))
} }
public func body(content: Content) -> some View { public func body(content: Content) -> some View {
content content
} }
public var animatableData: AnimatablePair<Angle.AnimatableData, UnitPoint.AnimatableData> {
get {
.init(angle.animatableData, anchor.animatableData)
}
set {
(angle.animatableData, anchor.animatableData) = newValue[]
}
}
} }
public extension View { public extension View {

View File

@ -0,0 +1,59 @@
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/9/21.
//
import Foundation
@frozen
public struct _ScaleEffect: GeometryEffect, Equatable {
public var scale: CGSize
public var anchor: UnitPoint
@inlinable
public init(scale: CGSize, anchor: UnitPoint = .center) {
self.scale = scale
self.anchor = anchor
}
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(.init(scaleX: scale.width, y: scale.height))
}
public func body(content: Content) -> some View {
content
}
}
public extension View {
@inlinable
func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View {
modifier(_ScaleEffect(scale: scale, anchor: anchor))
}
@inlinable
func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some View {
scaleEffect(CGSize(width: s, height: s), anchor: anchor)
}
@inlinable
func scaleEffect(
x: CGFloat = 1.0,
y: CGFloat = 1.0,
anchor: UnitPoint = .center
) -> some View {
scaleEffect(CGSize(width: x, height: y), anchor: anchor)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import Foundation
public struct _FlexFrameLayout: ViewModifier { public struct _FlexFrameLayout: ViewModifier {
public let minWidth: CGFloat? public let minWidth: CGFloat?
public let idealWidth: CGFloat? public let idealWidth: CGFloat?
@ -24,11 +26,11 @@ public struct _FlexFrameLayout: ViewModifier {
// These are special cases in SwiftUI, where the child // These are special cases in SwiftUI, where the child
// will request the entire width/height of the parent. // will request the entire width/height of the parent.
public var fillWidth: Bool { public var fillWidth: Bool {
minWidth == 0 && maxWidth == .infinity (minWidth == 0 || minWidth == nil) && maxWidth == .infinity
} }
public var fillHeight: Bool { public var fillHeight: Bool {
minHeight == 0 && maxHeight == .infinity (minHeight == 0 || minHeight == nil) && maxHeight == .infinity
} }
init( init(
@ -54,6 +56,10 @@ public struct _FlexFrameLayout: ViewModifier {
} }
} }
extension _FlexFrameLayout: Animatable {
public typealias AnimatableData = EmptyAnimatableData
}
public extension View { public extension View {
func frame( func frame(
minWidth: CGFloat? = nil, minWidth: CGFloat? = nil,

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import Foundation
public struct _FrameLayout: ViewModifier { public struct _FrameLayout: ViewModifier {
public let width: CGFloat? public let width: CGFloat?
public let height: CGFloat? public let height: CGFloat?
@ -28,6 +30,10 @@ public struct _FrameLayout: ViewModifier {
} }
} }
extension _FrameLayout: Animatable {
public typealias AnimatableData = EmptyAnimatableData
}
public extension View { public extension View {
func frame( func frame(
width: CGFloat? = nil, width: CGFloat? = nil,

View File

@ -0,0 +1,31 @@
// Copyright 2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Underscore is present in the name for SwiftUI compatibility.
public struct _HoverActionModifier: ViewModifier {
public var hover: ((Bool) -> ())?
public typealias Body = Never
}
extension ModifiedContent
where Content: View, Modifier == _HoverActionModifier
{
var hover: ((Bool) -> ())? { modifier.hover }
}
public extension View {
func onHover(perform action: ((Bool) -> ())?) -> some View {
modifier(_HoverActionModifier(hover: action))
}
}

View File

@ -14,11 +14,34 @@
// FIXME: these should have standalone implementations // FIXME: these should have standalone implementations
public extension View { public extension View {
@_spi(TokamakCore)
func _onMount(perform action: (() -> ())? = nil) -> some View { func _onMount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(appear: action)) modifier(_AppearanceActionModifier(appear: action))
} }
@_spi(TokamakCore)
func _onUpdate(perform action: (() -> ())? = nil) -> some View {
modifier(_LifecycleActionModifier(update: action))
}
@_spi(TokamakCore)
func _onUnmount(perform action: (() -> ())? = nil) -> some View { func _onUnmount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action)) modifier(_AppearanceActionModifier(disappear: action))
} }
} }
protocol LifecycleActionType {
var update: (() -> ())? { get }
}
struct _LifecycleActionModifier: ViewModifier {
var update: (() -> ())?
typealias Body = Never
}
extension ModifiedContent: LifecycleActionType
where Content: View, Modifier == _LifecycleActionModifier
{
var update: (() -> ())? { modifier.update }
}

View File

@ -13,12 +13,16 @@
// limitations under the License. // limitations under the License.
protocol ModifierContainer { protocol ModifierContainer {
var environmentModifier: EnvironmentModifier? { get } var environmentModifier: _EnvironmentModifier? { get }
} }
protocol ModifiedContentProtocol {}
/// A value with a modifier applied to it. /// A value with a modifier applied to it.
public struct ModifiedContent<Content, Modifier> { public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
@Environment(\.self) public var environment @Environment(\.self)
public var environment
public typealias Body = Never public typealias Body = Never
public private(set) var content: Content public private(set) var content: Content
public private(set) var modifier: Modifier public private(set) var modifier: Modifier
@ -30,7 +34,7 @@ public struct ModifiedContent<Content, Modifier> {
} }
extension ModifiedContent: ModifierContainer { extension ModifiedContent: ModifierContainer {
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier } var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
} }
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader { extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
@ -39,7 +43,7 @@ extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
} }
} }
extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier { extension ModifiedContent: View, GroupView, ParentView where Content: View, Modifier: ViewModifier {
public var body: Body { public var body: Body {
neverBody("ModifiedContent<View, ViewModifier>") neverBody("ModifiedContent<View, ViewModifier>")
} }

View File

@ -23,6 +23,32 @@ public extension View {
navigationTitle(title) navigationTitle(title)
} }
@available(
*,
deprecated,
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
)
func navigationBarTitle(
_ title: Text,
displayMode: NavigationBarItem.TitleDisplayMode
) -> some View {
navigationTitle(title)
.navigationBarTitleDisplayMode(displayMode)
}
@available(
*,
deprecated,
message: "Use navigationTitle(_:) with navigationBarTitleDisplayMode(_:)"
)
func navigationBarTitle<S: StringProtocol>(
_ title: S,
displayMode: NavigationBarItem.TitleDisplayMode
) -> some View {
navigationTitle(title)
.navigationBarTitleDisplayMode(displayMode)
}
func navigationTitle(_ title: Text) -> some View { func navigationTitle(_ title: Text) -> some View {
navigationTitle { title } navigationTitle { title }
} }
@ -36,4 +62,11 @@ public extension View {
{ {
preference(key: NavigationTitleKey.self, value: AnyView(title())) preference(key: NavigationTitleKey.self, value: AnyView(title()))
} }
func navigationBarTitleDisplayMode(
_ displayMode: NavigationBarItem
.TitleDisplayMode
) -> some View {
preference(key: NavigationBarItemKey.self, value: .init(displayMode: displayMode))
}
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import Foundation
public struct _PaddingLayout: ViewModifier { public struct _PaddingLayout: ViewModifier {
public var edges: Edge.Set public var edges: Edge.Set
public var insets: EdgeInsets? public var insets: EdgeInsets?
@ -26,17 +28,36 @@ public struct _PaddingLayout: ViewModifier {
} }
} }
extension _PaddingLayout: Animatable {
public typealias AnimatableData = EmptyAnimatableData
}
public extension View { public extension View {
func padding(_ insets: EdgeInsets) -> some View { func padding(_ insets: EdgeInsets) -> ModifiedContent<Self, _PaddingLayout> {
modifier(_PaddingLayout(insets: insets)) modifier(_PaddingLayout(insets: insets))
} }
func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View { func padding(
_ edges: Edge.Set = .all,
_ length: CGFloat? = nil
) -> ModifiedContent<Self, _PaddingLayout> {
let insets = length.map { EdgeInsets(_all: $0) } let insets = length.map { EdgeInsets(_all: $0) }
return modifier(_PaddingLayout(edges: edges, insets: insets)) return modifier(_PaddingLayout(edges: edges, insets: insets))
} }
func padding(_ length: CGFloat) -> some View { func padding(_ length: CGFloat) -> ModifiedContent<Self, _PaddingLayout> {
padding(.all, length) padding(.all, length)
} }
} }
public extension ModifiedContent where Modifier == _PaddingLayout, Content: View {
func padding(_ length: CGFloat) -> ModifiedContent<Content, _PaddingLayout> {
var layout = modifier
layout.insets?.top += length
layout.insets?.leading += length
layout.insets?.bottom += length
layout.insets?.trailing += length
return ModifiedContent(content: content, modifier: layout)
}
}

View File

@ -1,26 +1,106 @@
public struct _ShadowLayout: ViewModifier, EnvironmentReader { // Copyright 2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
public struct _ShadowEffect: EnvironmentalModifier, Equatable {
public var color: Color public var color: Color
public var radius: CGFloat public var radius: CGFloat
public var x: CGFloat public var offset: CGSize
public var y: CGFloat
public var environment: EnvironmentValues!
public func body(content: Content) -> some View { @inlinable
content init(
color: Color,
radius: CGFloat,
offset: CGSize
) {
self.color = color
self.radius = radius
self.offset = offset
} }
mutating func setContent(from values: EnvironmentValues) { public func resolve(in environment: EnvironmentValues) -> _Resolved {
environment = values .init(
color: color.provider.resolve(in: environment),
radius: radius,
offset: offset
)
}
public struct _Resolved: ViewModifier, Animatable {
public var color: AnyColorBox.ResolvedValue
public var radius: CGFloat
public var offset: CGSize
public func body(content: Content) -> some View {
content
}
public typealias AnimatableData = AnimatablePair<
AnimatablePair<
Float,
AnimatablePair<
Float,
AnimatablePair<Float, Float>
>
>,
AnimatablePair<CGFloat, CGSize.AnimatableData>
>
public var animatableData: _Resolved.AnimatableData {
get {
.init(
.init(
Float(color.red),
.init(
Float(color.green),
.init(
Float(color.blue),
Float(color.opacity)
)
)
),
.init(radius, offset.animatableData)
)
}
set {
color = .init(
red: Double(newValue[].0[].0),
green: Double(newValue[].0[].1[].0),
blue: Double(newValue[].0[].1[].1[].0),
opacity: Double(newValue[].0[].1[].1[].1),
space: .sRGB
)
(radius, offset.animatableData) = newValue[].1[]
}
}
} }
} }
public extension View { public extension View {
@inlinable
func shadow( func shadow(
color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
radius: CGFloat, radius: CGFloat,
x: CGFloat = 0, x: CGFloat = 0,
y: CGFloat = 0 y: CGFloat = 0
) -> some View { ) -> some View {
modifier(_ShadowLayout(color: color, radius: radius, x: x, y: y)) modifier(
_ShadowEffect(
color: color,
radius: radius,
offset: .init(width: x, height: y)
)
)
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,6 +15,29 @@
// Created by Carson Katri on 6/29/20. // Created by Carson Katri on 6/29/20.
// //
import Foundation
/// Override this View's body to provide a layout that fits the background to the content.
public struct _BackgroundLayout<Content, Background>: _PrimitiveView
where Content: View, Background: View
{
public let content: Content
public let background: Background
public let alignment: Alignment
@_spi(TokamakCore)
public init(content: Content, background: Background, alignment: Alignment) {
self.content = content
self.background = background
self.alignment = alignment
}
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(background)
visitor.visit(content)
}
}
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
where Background: View where Background: View
{ {
@ -28,11 +51,11 @@ public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
} }
public func body(content: Content) -> some View { public func body(content: Content) -> some View {
// FIXME: Clip to bounds of foreground. _BackgroundLayout(
ZStack(alignment: alignment) { content: content,
background background: background,
content alignment: alignment
} )
} }
mutating func setContent(from values: EnvironmentValues) { mutating func setContent(from values: EnvironmentValues) {
@ -56,6 +79,74 @@ public extension View {
) -> some View where Background: View { ) -> some View where Background: View {
modifier(_BackgroundModifier(background: background, alignment: alignment)) modifier(_BackgroundModifier(background: background, alignment: alignment))
} }
@inlinable
func background<V>(
alignment: Alignment = .center,
@ViewBuilder content: () -> V
) -> some View where V: View {
background(content(), alignment: alignment)
}
}
@frozen
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
where Style: ShapeStyle, Bounds: Shape
{
public var environment: EnvironmentValues!
public var style: Style
public var shape: Bounds
public var fillStyle: FillStyle
@inlinable
public init(style: Style, shape: Bounds, fillStyle: FillStyle) {
self.style = style
self.shape = shape
self.fillStyle = fillStyle
}
public func body(content: Content) -> some View {
content
.background(shape.fill(style, style: fillStyle))
}
public mutating func setContent(from values: EnvironmentValues) {
environment = values
}
}
public extension View {
@inlinable
func background<S, T>(
_ style: S,
in shape: T,
fillStyle: FillStyle = FillStyle()
) -> some View where S: ShapeStyle, T: Shape {
modifier(_BackgroundShapeModifier(style: style, shape: shape, fillStyle: fillStyle))
}
@inlinable
func background<S>(
in shape: S,
fillStyle: FillStyle = FillStyle()
) -> some View where S: Shape {
background(BackgroundStyle(), in: shape, fillStyle: fillStyle)
}
}
/// Override this View's body to provide a layout that fits the background to the content.
public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
where Content: View, Overlay: View
{
public let content: Content
public let overlay: Overlay
public let alignment: Alignment
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
visitor.visit(content)
visitor.visit(overlay)
}
} }
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
@ -71,11 +162,11 @@ public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
} }
public func body(content: Content) -> some View { public func body(content: Content) -> some View {
// FIXME: Clip to content shape. _OverlayLayout(
ZStack(alignment: alignment) { content: content,
content overlay: overlay,
overlay alignment: alignment
} )
} }
mutating func setContent(from values: EnvironmentValues) { mutating func setContent(from values: EnvironmentValues) {
@ -96,6 +187,21 @@ public extension View {
modifier(_OverlayModifier(overlay: overlay, alignment: alignment)) modifier(_OverlayModifier(overlay: overlay, alignment: alignment))
} }
@inlinable
func overlay<V>(
alignment: Alignment = .center,
@ViewBuilder content: () -> V
) -> some View where V: View {
modifier(_OverlayModifier(overlay: content(), alignment: alignment))
}
@inlinable
func overlay<S>(
_ style: S
) -> some View where S: ShapeStyle {
overlay(Rectangle().fill(style))
}
func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle { func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle {
overlay(Rectangle().strokeBorder(content, lineWidth: width)) overlay(Rectangle().strokeBorder(content, lineWidth: width))
} }

View File

@ -0,0 +1,28 @@
// Copyright 2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
public extension View {
func task(
priority: TaskPriority = .userInitiated,
_ action: @escaping @Sendable () async -> ()
) -> some View {
var task: Task<(), Never>?
return onAppear {
task = Task(priority: priority, operation: action)
}
.onDisappear {
task?.cancel()
}
}
}

View File

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

View File

@ -15,33 +15,51 @@
// Created by Carson Katri on 7/19/20. // Created by Carson Katri on 7/19/20.
// //
import CombineShim import OpenCombineShim
// This is very similar to `MountedCompositeView`. However, the `mountedBody` // This is very similar to `MountedCompositeView`. However, the `mountedBody`
// is the computed content of the specified `Scene`, instead of having child // is the computed content of the specified `Scene`, instead of having child
// `View`s // `View`s
final class MountedApp<R: Renderer>: MountedCompositeElement<R> { final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
override func mount( override func mount(
before _: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on _: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
with transaction: Transaction
) { ) {
super.prepareForMount(with: transaction)
// `App` elements have no siblings, hence the `before` argument is discarded. // `App` elements have no siblings, hence the `before` argument is discarded.
// They also have no parents, so the `parent` argument is discarded as well. // They also have no parents, so the `parent` argument is discarded as well.
let childBody = reconciler.render(mountedApp: self) let childBody = reconciler.render(mountedApp: self)
let child: MountedElement<R> = mountChild(childBody) let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
mountedChildren = [child] mountedChildren = [child]
child.mount(before: nil, on: self, with: reconciler) child.transaction = transaction
child.mount(before: nil, on: self, in: reconciler, with: transaction)
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
} }
override func unmount(with reconciler: StackReconciler<R>) { override func unmount(
mountedChildren.forEach { $0.unmount(with: reconciler) } in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
mountedChildren
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
} }
private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> { /// Mounts a child scene within the app.
/// - Parameters:
/// - renderer: An instance conforming to the `Renderer` protocol to render the mounted
/// scene with.
/// - childBody: The body of the child scene to mount for this app.
/// - Returns: Returns an instance of the `MountedScene` class that's already mounted in this app.
private func mountChild(_ renderer: R, _ childBody: _AnyScene) -> MountedScene<R> {
let mountedScene: MountedScene<R> = childBody let mountedScene: MountedScene<R> = childBody
.makeMountedScene(parentTarget, environmentValues, self) .makeMountedScene(renderer, parentTarget, environmentValues, self)
if let title = mountedScene.title { if let title = mountedScene.title {
// swiftlint:disable force_cast // swiftlint:disable force_cast
(app.type as! _TitledApp.Type)._setTitle(title) (app.type as! _TitledApp.Type)._setTitle(title)
@ -49,17 +67,19 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
return mountedScene return mountedScene
} }
override func update(with reconciler: StackReconciler<R>) { override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
let element = reconciler.render(mountedApp: self) let element = reconciler.render(mountedApp: self)
reconciler.reconcile( reconciler.reconcile(
self, self,
with: element, with: element,
transaction: transaction,
getElementType: { $0.type }, getElementType: { $0.type },
updateChild: { updateChild: {
$0.environmentValues = environmentValues $0.environmentValues = environmentValues
$0.scene = _AnyScene(element) $0.scene = _AnyScene(element)
$0.transaction = transaction
}, },
mountChild: { mountChild($0) } mountChild: { mountChild(reconciler.renderer, $0) }
) )
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2018-2020 Tokamak contributors // Copyright 2018-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/19/20. // Created by Carson Katri on 7/19/20.
// //
import CombineShim import OpenCombineShim
class MountedCompositeElement<R: Renderer>: MountedElement<R> { class MountedCompositeElement<R: Renderer>: MountedElement<R> {
let parentTarget: R.TargetType let parentTarget: R.TargetType
@ -61,10 +61,11 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
_ view: AnyView, _ view: AnyView,
_ parentTarget: R.TargetType, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues, _ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>? _ parent: MountedElement<R>?
) { ) {
self.parentTarget = parentTarget self.parentTarget = parentTarget
super.init(view, environmentValues, parent) super.init(view, environmentValues, viewTraits, parent)
} }
} }

View File

@ -15,25 +15,43 @@
// Created by Max Desiatov on 03/12/2018. // Created by Max Desiatov on 03/12/2018.
// //
import CombineShim import OpenCombineShim
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> { final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
override func mount( override func mount(
before sibling: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
with transaction: Transaction
) { ) {
super.prepareForMount(with: transaction)
var transaction = transaction
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
// Disable animations on mount so `animation(_:)` doesn't try to animate
// until the transition finishes.
transaction.disablesAnimations = true
self.transaction = transaction
updateVariadicView()
let childBody = reconciler.render(compositeView: self) let childBody = reconciler.render(compositeView: self)
if let traitModifier = view.view as? _TraitWritingModifierProtocol {
traitModifier.modifyViewTraitStore(&viewTraits)
}
let child: MountedElement<R> = childBody.makeMountedView( let child: MountedElement<R> = childBody.makeMountedView(
reconciler.renderer,
parentTarget, parentTarget,
environmentValues, environmentValues,
viewTraits,
self self
) )
mountedChildren = [child] mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler) child.mount(before: sibling, on: self, in: reconciler, with: transaction)
// `_TargetRef` is a composite view, so it's enough to check for it only here // `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite
// view, so it's enough check for it only here.
if var targetRef = view.view as? TargetRefType { if var targetRef = view.view as? TargetRefType {
// `_TargetRef` body is not always a host view that has a target, need to traverse // `_TargetRef` body is not always a host view that has a target, need to traverse
// all descendants to find a `MountedHostView<R>` instance. // all descendants to find a `MountedHostView<R>` instance.
@ -51,7 +69,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
reconciler.afterCurrentRender(perform: { [weak self] in reconciler.afterCurrentRender(perform: { [weak self] in
guard let self = self else { return } guard let self = self else { return }
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to // FIXME: this has to be implemented in a renderer-specific way, otherwise it's equivalent to
// `_onMount` and `_onUnmount` at the moment, // `_onMount` and `_onUnmount` at the moment,
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details // see https://github.com/swiftwasm/Tokamak/issues/175 for more details
if let appearanceAction = self.view.view as? AppearanceActionType { if let appearanceAction = self.view.view as? AppearanceActionType {
@ -60,36 +78,104 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol { if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore) self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
if let parent = parent {
parent.preferenceStore.merge(with: self.preferenceStore)
}
} }
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol { if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
preferenceReader.preferenceStore(self.preferenceStore) preferenceReader.preferenceStore(self.preferenceStore)
} }
}) })
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
} }
override func unmount(with reconciler: StackReconciler<R>) { override func unmount(
mountedChildren.forEach { $0.unmount(with: reconciler) } in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
var transaction = transaction
transaction.disablesAnimations = false
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
mountedChildren.forEach {
$0.viewTraits = self.viewTraits
$0.unmount(in: reconciler, with: transaction, parentTask: parentTask)
}
if let appearanceAction = view.view as? AppearanceActionType { if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.disappear?() appearanceAction.disappear?()
} }
} }
override func update(with reconciler: StackReconciler<R>) { override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
var transaction = transaction
transaction.disablesAnimations = false
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
updateVariadicView()
let element = reconciler.render(compositeView: self) let element = reconciler.render(compositeView: self)
reconciler.reconcile( reconciler.reconcile(
self, self,
with: element, with: element,
transaction: transaction,
getElementType: { $0.type }, getElementType: { $0.type },
updateChild: { updateChild: {
$0.environmentValues = environmentValues $0.environmentValues = environmentValues
$0.view = AnyView(element) $0.view = AnyView(element)
$0.transaction = transaction
}, },
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) } mountChild: {
$0.makeMountedView(
reconciler.renderer,
parentTarget,
environmentValues,
viewTraits,
self
)
}
) )
if let lifecycleActions = view.view as? LifecycleActionType {
lifecycleActions.update?()
}
}
private func updateVariadicView() {
if var tree = view.view as? _VariadicView_AnyTree {
let elements = ((tree.anyContent.view as? GroupView)?.recursiveChildren ?? [tree.anyContent])
.enumerated()
.map { (pair: EnumeratedSequence<[AnyView]>.Element) -> _VariadicView_Children.Element in
var viewTraits = _ViewTraitStore(values: [:])
if let traitModifier = pair.element.view as? _TraitWritingModifierProtocol {
traitModifier.modifyViewTraitStore(&viewTraits)
}
return _VariadicView_Children.Element(
view: pair.element,
id: AnyHashable(pair.offset),
// TODO: Retrieve the ID from the `IDView`. Maybe this should use traits too.
viewTraits: viewTraits,
onTraitsUpdated: { _ in }
)
}
tree.children = _VariadicView_Children(elements: elements)
view.view = tree
}
}
}
private extension GroupView {
var recursiveChildren: [AnyView] {
var allChildren = [AnyView]()
for child in children {
if !(child.view is ModifiedContentProtocol),
let group = child.view as? GroupView
{
allChildren.append(contentsOf: group.recursiveChildren)
} else {
allChildren.append(child)
}
}
return allChildren
} }
} }

View File

@ -85,35 +85,51 @@ public class MountedElement<R: Renderer> {
} }
var mountedChildren = [MountedElement<R>]() var mountedChildren = [MountedElement<R>]()
var environmentValues: EnvironmentValues
unowned var parent: MountedElement<R>? public var transaction: Transaction = .init(animation: nil)
/// `didSet` on this field propagates the preference changes up the view tree. /// Where this element is the process of mounting/unmounting.
var preferenceStore: _PreferenceStore = .init() { var transitionPhase = TransitionPhase.willMount
didSet { /// The current `UnmountTask` of this element.
parent?.preferenceStore.merge(with: preferenceStore) var unmountTask: UnmountTask<R>?
}
} public internal(set) var environmentValues: EnvironmentValues
private(set) weak var parent: MountedElement<R>?
var preferenceStore: _PreferenceStore = .init()
public internal(set) var viewTraits: _ViewTraitStore
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) { init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .app(app) element = .app(app)
self.parent = parent self.parent = parent
self.environmentValues = environmentValues self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) { init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .scene(scene) element = .scene(scene)
self.parent = parent self.parent = parent
self.environmentValues = environmentValues self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) { init(
_ view: AnyView,
_ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>?
) {
element = .view(view) element = .view(view)
self.parent = parent self.parent = parent
self.environmentValues = environmentValues self.environmentValues = environmentValues
self.viewTraits = viewTraits
updateEnvironment() updateEnvironment()
connectParentPreferenceStore()
} }
func updateEnvironment() { func updateEnvironment() {
@ -128,19 +144,75 @@ public class MountedElement<R: Renderer> {
} }
} }
func connectParentPreferenceStore() {
preferenceStore.parent = parent?.preferenceStore
}
/// You must call `super.prepareForMount` before all other mounting work.
func prepareForMount(with transaction: Transaction) {
// `GroupView`'s don't really mount, so let their children transition if the group can.
if case let .view(view) = element,
view.type is GroupView.Type
{
transitionPhase = parent?.transitionPhase ?? .normal
}
// Allow the root of a mount to transition
// (if their parent isn't mounting, then they are the root of the mount).
if parent?.transitionPhase == .normal {
viewTraits.insert(
transaction.animation != nil
|| _AnyTransitionProxy(viewTraits.transition)
.resolve(in: environmentValues)
.insertionAnimation != nil,
forKey: CanTransitionTraitKey.self
)
}
}
/// You must call `super.mount` after all other mounting work.
func mount( func mount(
before sibling: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
with transaction: Transaction
) { ) {
fatalError("implement \(#function) in subclass") // Set the phase to `normal` after finished mounting.
transitionPhase = .normal
} }
func unmount(with reconciler: StackReconciler<R>) { /// You must call `super.unmount` before all other unmounting work.
fatalError("implement \(#function) in subclass") func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
if !(self is MountedHostView<R>) {
unmountTask = parentTask?.appendChild()
}
// `GroupView`'s don't really unmount, so let their children transition if the group can.
if case let .view(view) = element,
view.type is GroupView.Type
{
transitionPhase = parent?.transitionPhase ?? .normal
} else {
// Set the phase to `willUnmount` before unmounting.
transitionPhase = .willUnmount
}
// Allow the root of an unmount to transition
// (if their parent isn't unmounting, then they are the root of the unmount).
if parent?.transitionPhase == .normal {
viewTraits.insert(
transaction.animation != nil
|| _AnyTransitionProxy(viewTraits.transition)
.resolve(in: environmentValues)
.removalAnimation != nil,
forKey: CanTransitionTraitKey.self
)
}
} }
func update(with reconciler: StackReconciler<R>) { func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
fatalError("implement \(#function) in subclass") fatalError("implement \(#function) in subclass")
} }
@ -222,16 +294,18 @@ extension TypeInfo {
extension AnyView { extension AnyView {
func makeMountedView<R: Renderer>( func makeMountedView<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues, _ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>? _ parent: MountedElement<R>?
) -> MountedElement<R> { ) -> MountedElement<R> {
if type == EmptyView.self { if type == EmptyView.self {
return MountedEmptyView(self, environmentValues, parent) return MountedEmptyView(self, environmentValues, viewTraits, parent)
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) { } else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
return MountedHostView(self, parentTarget, environmentValues, parent) return MountedHostView(self, parentTarget, environmentValues, viewTraits, parent)
} else { } else {
return MountedCompositeView(self, parentTarget, environmentValues, parent) return MountedCompositeView(self, parentTarget, environmentValues, viewTraits, parent)
} }
} }
} }

View File

@ -19,10 +19,20 @@ final class MountedEmptyView<R: Renderer>: MountedElement<R> {
override func mount( override func mount(
before sibling: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
) {} with transaction: Transaction
) {
super.prepareForMount(with: transaction)
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
}
override func unmount(with reconciler: StackReconciler<R>) {} override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
}
override func update(with reconciler: StackReconciler<R>) {} override func update(in reconciler: StackReconciler<R>, with transaction: Transaction?) {}
} }

View File

@ -33,19 +33,25 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
_ view: AnyView, _ view: AnyView,
_ parentTarget: R.TargetType, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues, _ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>? _ parent: MountedElement<R>?
) { ) {
self.parentTarget = parentTarget self.parentTarget = parentTarget
super.init(view, environmentValues, parent) super.init(view, environmentValues, viewTraits, parent)
} }
override func mount( override func mount(
before sibling: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
with transaction: Transaction
) { ) {
guard let target = reconciler.renderer?.mountTarget( super.prepareForMount(with: transaction)
self.transaction = transaction
guard let target = reconciler.renderer.mountTarget(
before: sibling, before: sibling,
to: parentTarget, to: parentTarget,
with: self with: self
@ -56,8 +62,17 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
guard !view.children.isEmpty else { return } guard !view.children.isEmpty else { return }
let isGroupView = view.type is GroupView.Type
// Don't allow children to transition their mounting since they aren't individually
// appearing (unless its a `GroupView`, which is flattened).
mountedChildren = view.children.map { mountedChildren = view.children.map {
$0.makeMountedView(target, environmentValues, self) $0.makeMountedView(
reconciler.renderer,
target,
environmentValues,
isGroupView ? self.viewTraits : .init(),
self
)
} }
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of /* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
@ -65,44 +80,73 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
`GroupView`. `GroupView`.
*/ */
let isGroupView = view.type is GroupView.Type
mountedChildren.forEach { mountedChildren.forEach {
$0.mount(before: isGroupView ? sibling : nil, on: self, with: reconciler) $0.mount(before: isGroupView ? sibling : nil, on: self, in: reconciler, with: transaction)
} }
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
} }
override func unmount(with reconciler: StackReconciler<R>) { private var parentUnmountTask = UnmountTask<R>()
override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
guard let target = target else { return } guard let target = target else { return }
reconciler.renderer?.unmount( let task = UnmountHostTask(self, in: reconciler) {
self.mountedChildren.forEach {
$0.unmount(in: reconciler, with: transaction, parentTask: self.unmountTask)
}
}
task.isCancelled = parentTask?.isCancelled ?? false
unmountTask = task
parentTask?.childTasks.append(task)
reconciler.renderer.unmount(
target: target, target: target,
from: parentTarget, from: parentTarget,
with: self with: task
) { )
self.mountedChildren.forEach { $0.unmount(with: reconciler) }
}
} }
override func update(with reconciler: StackReconciler<R>) { /// Stop any unfinished unmounts and complete them without transitions.
private func invalidateUnmount() {
parentUnmountTask.cancel()
parentUnmountTask.completeImmediately()
parentUnmountTask = .init()
}
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
guard let target = target else { return } guard let target = target else { return }
invalidateUnmount()
updateEnvironment() updateEnvironment()
target.view = view target.view = view
reconciler.renderer?.update(target: target, with: self) reconciler.renderer.update(target: target, with: self)
var childrenViews = view.children var childrenViews = view.children
let traits = view.type is GroupView.Type ? viewTraits : .init()
switch (mountedChildren.isEmpty, childrenViews.isEmpty) { switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
// if existing children present and new children array is empty // if existing children present and new children array is empty
// then unmount all existing children // then unmount all existing children
case (false, true): case (false, true):
mountedChildren.forEach { $0.unmount(with: reconciler) } mountedChildren.forEach {
$0.unmount(in: reconciler, with: transaction, parentTask: self.parentUnmountTask)
}
mountedChildren = [] mountedChildren = []
// if no existing children then mount all new children // if no existing children then mount all new children
case (true, false): case (true, false):
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) } mountedChildren = childrenViews.map {
mountedChildren.forEach { $0.mount(on: self, with: reconciler) } $0.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
}
mountedChildren.forEach { $0.mount(on: self, in: reconciler, with: transaction) }
// if both arrays have items then reconcile by types and keys // if both arrays have items then reconcile by types and keys
case (false, false): case (false, false):
@ -117,16 +161,24 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
mountedChild.environmentValues = environmentValues mountedChild.environmentValues = environmentValues
mountedChild.view = childView mountedChild.view = childView
mountedChild.updateEnvironment() mountedChild.updateEnvironment()
mountedChild.update(with: reconciler) mountedChild.update(in: reconciler, with: transaction)
newChild = mountedChild newChild = mountedChild
} else { } else {
/* note the order of operations here: we mount the new child first, use the mounted child /* note the order of operations here: we mount the new child first, use the mounted child
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
by unmounting it. by unmounting it.
*/ */
newChild = childView.makeMountedView(target, environmentValues, self) newChild = childView.makeMountedView(
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler) reconciler.renderer,
mountedChild.unmount(with: reconciler) target,
environmentValues,
traits,
self
)
newChild.mount(
before: mountedChild.firstDescendantTarget, on: self, in: reconciler, with: transaction
)
mountedChild.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
} }
newChildren.append(newChild) newChildren.append(newChild)
mountedChildren.removeFirst() mountedChildren.removeFirst()
@ -137,15 +189,15 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
// unmount remaining `mountedChildren` // unmount remaining `mountedChildren`
if !mountedChildren.isEmpty { if !mountedChildren.isEmpty {
for child in mountedChildren { for child in mountedChildren {
child.unmount(with: reconciler) child.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
} }
} else { } else {
// more views left than children were mounted, // more views left than children were mounted,
// mount remaining views // mount remaining views
for firstChild in childrenViews { for firstChild in childrenViews {
let newChild: MountedElement<R> = let newChild: MountedElement<R> =
firstChild.makeMountedView(target, environmentValues, self) firstChild.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
newChild.mount(on: self, with: reconciler) newChild.mount(on: self, in: reconciler, with: transaction)
newChildren.append(newChild) newChildren.append(newChild)
} }
} }

View File

@ -31,25 +31,36 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
override func mount( override func mount(
before sibling: R.TargetType? = nil, before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil, on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R> in reconciler: StackReconciler<R>,
with transaction: Transaction
) { ) {
super.prepareForMount(with: transaction)
let childBody = reconciler.render(mountedScene: self) let childBody = reconciler.render(mountedScene: self)
let child: MountedElement<R> = childBody let child: MountedElement<R> = childBody
.makeMountedElement(parentTarget, environmentValues, self) .makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
mountedChildren = [child] mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler) child.mount(before: sibling, on: self, in: reconciler, with: transaction)
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
} }
override func unmount(with reconciler: StackReconciler<R>) { override func unmount(
mountedChildren.forEach { $0.unmount(with: reconciler) } in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
mountedChildren
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
} }
override func update(with reconciler: StackReconciler<R>) { override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
let element = reconciler.render(mountedScene: self) let element = reconciler.render(mountedScene: self)
reconciler.reconcile( reconciler.reconcile(
self, self,
with: element, with: element,
transaction: transaction,
getElementType: { $0.type }, getElementType: { $0.type },
updateChild: { updateChild: {
$0.environmentValues = environmentValues $0.environmentValues = environmentValues
@ -59,8 +70,11 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
case let .view(view): case let .view(view):
$0.view = AnyView(view) $0.view = AnyView(view)
} }
$0.transaction = transaction
}, },
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) } mountChild: {
$0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
}
) )
} }
} }
@ -76,21 +90,23 @@ extension _AnyScene.BodyResult {
} }
func makeMountedElement<R: Renderer>( func makeMountedElement<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues, _ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>? _ parent: MountedElement<R>?
) -> MountedElement<R> { ) -> MountedElement<R> {
switch self { switch self {
case let .scene(scene): case let .scene(scene):
return scene.makeMountedScene(parentTarget, environmentValues, parent) return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent)
case let .view(view): case let .view(view):
return view.makeMountedView(parentTarget, environmentValues, parent) return view.makeMountedView(renderer, parentTarget, environmentValues, .init(), parent)
} }
} }
} }
extension _AnyScene { extension _AnyScene {
func makeMountedScene<R: Renderer>( func makeMountedScene<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues, _ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>? _ parent: MountedElement<R>?
@ -104,11 +120,17 @@ extension _AnyScene {
let children: [MountedElement<R>] let children: [MountedElement<R>]
if let deferredScene = scene as? SceneDeferredToRenderer { if let deferredScene = scene as? SceneDeferredToRenderer {
children = [ children = [
deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent), deferredScene.deferredBody.makeMountedView(
renderer,
parentTarget,
environmentValues,
.init(),
parent
),
] ]
} else if let groupScene = scene as? GroupScene { } else if let groupScene = scene as? GroupScene {
children = groupScene.children.map { children = groupScene.children.map {
$0.makeMountedScene(parentTarget, environmentValues, parent) $0.makeMountedScene(renderer, parentTarget, environmentValues, parent)
} }
} else { } else {
children = [] children = []

View File

@ -0,0 +1,81 @@
// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/19/21.
//
/// A tree of cancellable in-progress unmounts.
public class UnmountTask<R> where R: Renderer {
public internal(set) var isCancelled = false
var childTasks = [UnmountTask<R>]()
private let callback: () -> ()
init(_ callback: @escaping () -> () = {}) {
self.callback = callback
}
func cancel() {
forEach { $0.isCancelled = true }
}
/// Call after completely unmounting the `host`.
public func finish() {
callback()
}
/// Adds and returns a new child `UnmountTask`
func appendChild() -> UnmountTask<R> {
let child = UnmountTask()
child.isCancelled = isCancelled
childTasks.append(child)
return child
}
/// Forces the element and all child tasks to unmount without transition.
func completeImmediately() {
forEach {
guard $0 is UnmountHostTask<R> else { return }
$0.completeImmediately()
}
}
func forEach(_ f: (UnmountTask<R>) -> ()) {
var stack = [self]
while let last = stack.popLast() {
f(last)
stack.insert(contentsOf: last.childTasks, at: 0)
}
}
}
/// The state for the unmounting of a `MountedHostView` by a `Renderer`.
public final class UnmountHostTask<R>: UnmountTask<R> where R: Renderer {
public private(set) weak var host: MountedHostView<R>!
private unowned var reconciler: StackReconciler<R>
init(
_ host: MountedHostView<R>,
in reconciler: StackReconciler<R>,
callback: @escaping () -> ()
) {
self.host = host
self.reconciler = reconciler
super.init(callback)
}
override func completeImmediately() {
host.viewTraits.insert(false, forKey: CanTransitionTraitKey.self)
host.unmount(in: reconciler, with: .init(animation: nil), parentTask: nil)
}
}

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