Compare commits

...

206 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
Mathew Polzin ea94ef9bcc
Simple Code Coverage analysis (#378)
Closes https://github.com/TokamakUI/Tokamak/issues/350.

Adds simple code coverage analysis. Until the GitHub token is set up in this repo, you can see the results including a comment on the PR here: https://github.com/mattpolzin/Tokamak/pull/2

* Adding codecov CI workflow.

* doh. forgot to skip build.

* drop unneeded llvm env var from build invocation. make comment always run.

* try a way to update a previous comment

* try to get comments to run on failure.

* use bug fix to codecov action.

* fix comment search string and drop min coverage

* use replace on comment updates

* attempt at diffing coverage from main branch

* comment out tests to affect test coverage

* try method for multiline outputs

* do i need to put it in its own script file

* right, switch coverage gen order to have branch checked out second.

* attempt to get code quotation around diff output.

* switch to git diff

* add note about the new script.

* try echoing a warning.

* post warning on success and error on failure.

* uncomment tests
2021-02-05 11:47:56 +00:00
Max Desiatov 53b474c33f
Delete unused `codecov.yml` config 2021-02-01 22:08:59 +00:00
Max Desiatov 5d951ad0af
Delete unused `codecov.sh` script 2021-02-01 22:08:39 +00:00
Max Desiatov d75b185553
Add checks for metadata state (#375)
* Add checks for metadata state

Type metadata actually *is* mutable, is updated by the runtime, and we need to be careful when reading from it.

* Add a precondition for type metadata state

* Replace `precondition` with an `assert`

* Add missing license headers
2021-02-01 10:02:49 +00:00
Max Desiatov 9a79548312
Use upstream OpenCombine instead of a fork (#377) 2021-01-30 10:26:30 +00:00
Max Desiatov 30b8d46aa8
Update JavaScriptKit, OpenCombineJS dependencies (#376) 2021-01-29 07:53:21 +00:00
Max Desiatov 0e89ea9529
Clean up metadata reflection code (#372)
Our OpenCombine fork no longer depends on Runtime, and we don't need much from it other than struct metadata. I removed the unused bits and bobs and kept only a minimal subset of it that we really need. This should make it easier for us to test and debug, as #367 has shown that some weird stuff may still lurk in that area.

* Add a test for environment injection

We had some issues in this code area previously and I'm thinking of refactoring it in attempt to fix #367. Would be great to increase the test coverage here before further refactoring.

* Update copyright years in `MountedElement.swift`

* Update copyright years in the rest of the files

* Vend the Runtime library directly

* Remove unused class, enum, tuple, func reflection

* Remove unused models and protocol metadata

* Remove unused MetadataType and NominalMetadataType

* Remove unused protocols, rename RelativePointer

* Remove more unused protocols

* Use immutable pointers for reflection

* Update copyright headers
2021-01-27 18:24:04 +00:00
Max Desiatov 07ccef88e1
Add @foscomputerservices to the list of maintainers (#373)
David has previously submitted super useful bug reports and fixes, and his experience in porting larger projects from SwiftUI to Tokamak is very valuable. Welcome to the team!
2021-01-25 20:08:53 +00:00
Max Desiatov 192c43b140
Refactor environment injection, add a test (#371)
* Add a test for environment injection

We had some issues in this code area previously and I'm thinking of refactoring it in attempt to fix #367. Would be great to increase the test coverage here before further refactoring.

* Update copyright years in `MountedElement.swift`

* Update copyright years in the rest of the files
2021-01-25 11:39:09 +00:00
Max Desiatov e04b7934fb
Replace uses of the Runtime library with stdlib (#370)
This should allow us to remove the Runtime dependency eventually, which seems to be unstable, especially across different platforms and Swift versions.

Seems to resolve in one instance https://github.com/TokamakUI/Tokamak/issues/367. There are a few other places where `typeInfo` is still used, I'll clean that up in a follow-up PR.

* Replace uses of the Runtime library with stdlib

* Remove irrelevant Runtime library imports

* Add TokamakCoreBenchmark target
2021-01-24 15:26:51 +00:00
Max Desiatov 9549282e53
Use `macos-latest` agent for the GTK build (#360)
`macos-latest` still points to macOS 10.15 right now, which has more agent's capacity, and macOS 11.0 agents are slow and unreliable. At the dame time, we have to keep using macOS 11.0 for the core build to have access to the new `App` lifecycle in the native demo project.
2021-01-22 12:54:49 +00:00
Max Desiatov 6f0528fe06
Add a benchmark target and a script to run it (#365)
* Add a benchmark target and a script to run it

Benchmarks need to be built in the release mode, that's why I created a separate `benchmark.sh` script to build it and run it.

I've also cleaned up a compiler warning in `TextEditor.swift` and bumped macOS agents to use Xcode 12.3 instead of 12.2.

* Benchmark `App` and `WindowGroup` rendering

* Add a `benchmark` task to `tasks.json`

* Exit `NativeDemo` directory before benchmarking
2021-01-20 21:09:54 +00:00
Max Desiatov 67aea3cc3b
Fix crashes in views with optional content (#364)
* Add TokamakStaticHTMLTests target

* Add AnyOptional, clarify conformances issues
2021-01-20 08:07:01 +03:00
Morten Bek Ditlevsen 163005dfe0
Add GTK support for `SecureField` (#363) 2021-01-19 18:20:39 +01:00
David Hunt ee4e8debc1
Two infinite loop fixes (#359)
Both of these issues are fixes to `CustomStringConvertible` implementations that either directly or indirectly called themselves via `String(describing:)`.
2021-01-19 15:13:48 +03:00
Morten Bek Ditlevsen 9199a90551
Added TextField support for GTK using GtkEntry (#361)
* Added TextField to TokamakGTK - WIP

* Made TextView update on gtk entry changes
2021-01-19 12:47:06 +01:00
Benjamin Kindle 5ca914818c
Add support for shadow modifier (#355)
* Add support for shadow modifier

Closes #324

* Convert radius to match iOS shadows closer

* Use correct environment values

* Include shadow demo in XCode project
2021-01-19 09:12:43 +00:00
David Hunt 1330b5306b
Removed an extra space that cause Safari to issue "Invalid value" errors and not load the pages (#358)
An extra space was added at the beginning of the 'ry' attribute value of a rect.  Safari reported: Error: Invalid value for <rect> attribute ry=" 25.0".
2021-01-18 16:22:08 +03:00
David Hunt 6955e56f77
Fixed a small issue with re-renderers being dropped (#356)
The code was clearing the queuedRerenders set after processing all of the updates on each re-renderer.  However, during processing it is possible that new values were enqueued.  By clearing the set afterwards, these newly queued re-renderers were accidentally dropped.

This would cause some Views to not update when @State was changed.
2021-01-18 15:39:03 +03:00
Max Desiatov e893e7ad8d
Add @mortenbekditlevsen to the list of maintainers in `README.md` (#352)
Just formalizing what was already a fact for a while, Morten has made some fantastic contributions to the GTK renderer!
2021-01-15 13:52:44 +00:00
Max Desiatov 3a0f8a8dd9
Build the GTK renderer on Ubuntu on CI (#347)
* Build the GTK renderer on Ubuntu on CI

* Add `--enable-test-discovery` flag to `Makefile`

* Use OpenCombine branch w/ no Runtime dependency

* Run `sudo apt-get update` on Ubuntu hosts

* Update OpenCombine dependency

* Pin OpenCombineJS dependency

* Update label.yml
2021-01-13 10:40:28 +00:00
Max Desiatov 362be5a5fa
Add missing `Link` re-export to TokamakDOM (#351) 2021-01-13 01:05:13 +00:00
Morten Bek Ditlevsen 25e2191154
GTK shape support WIP (#348)
* GTK shape support

* Support for ellipsis

* Allow drawing 'outside' of current frame. Experimental.

* Correctly support Capsules (rounded rects with nil cornerSize)

* Use boundingRect.size for Path size

* Refactored GdkRGBA from AnyColorBox.ResolvedValue to be reusable

* Added 'resolveToCairo(in environment:)' extension on Color

* Create slightly lighter View type hierarchies.
2021-01-12 12:09:25 +01:00
Max Desiatov b55f703972
Add a "bug report" issue template (#349)
* Add a "bug report" issue template

We currently receive bug reports that don't always provide a self-contained code sample or screenshots to help us reproduce issues quickly or to add test cases. I hope that this issue template alleviates that.

* Apply suggestions from code review

Co-authored-by: Jed Fox <git@jedfox.com>

Co-authored-by: Jed Fox <git@jedfox.com>
2021-01-08 14:53:17 +00:00
Morten Bek Ditlevsen a97a05ffd2
Allow gtk modifiers to be expressed as css attributes. (#345) 2020-12-31 23:15:00 +01:00
Max Desiatov c9877dcbd7
GTK: `background` modifier, support widget updates in `WidgetView` (#344)
Re-created #340.

* Added background modifier support for color backgrounds

* Fix indentation

* Allow WidgetView to be initialized with an update closure in order to fix updates to children of WidgetViews

* Fix indentation

Co-authored-by: Morten Bek Ditlevsen <morten@ka-ching.dk>
2020-12-26 19:22:16 +00:00
Max Desiatov 6ef59293f5
Add `Image` implementation to the GTK renderer (#343)
* Add rudimentary Image support

* Added Image implementation and sample image

* Updated sample and scaled down image

* Removed commented code

* Update main.swift

Co-authored-by: Morten Bek Ditlevsen <morten@ka-ching.dk>
2020-12-26 19:21:01 +00:00
Max Desiatov 99bcfd12b9
Add `GTK renderer` to the list of supported labels 2020-12-26 16:26:10 +00:00
Max Desiatov bd38866cb2
Add basic GTK renderer code (#333)
Based on the work discussed in #306.

* TokamakGTK implementation

* Fix macOS GTK Renderer impl

* Always release text in Picker. Use 'destroy_data' parameter to release closure boxes in GSignal.swift

* Revert commenting out this code

* Specify the product explicitly in Makefile

* Add GTK renderer build for macOS on CI

* Prevent xcodebuild from seeing GTK code

Co-authored-by: Carson Katri <carson.katri@gmail.com>
Co-authored-by: Morten Bek Ditlevsen <morten@ka-ching.dk>
2020-12-26 16:11:06 +00:00
Jed Fox 8e5ad7f67f
Remove extra `path` element (#341)
The blue capsule in the PathDemo, rendered to HTML:

```diff
 <svg style="width: 100%;height: 100%; overflow: visible;">
   <rect
     x="0.0"
     height="100%"
     width="100%"
     y="0.0"
     ry="50%"
     stroke-width="0.0"
   ></rect>
-  <path style="stroke-width: 0.0;" d=""></path>
 </svg>
```

* Run swiftformat

* Remove excess `path` element
2020-12-22 23:03:00 +00:00
Max Desiatov 3c97be617a
Update script injection code in `README.md` (#332)
This code had a missing `document` reference, and was plain outdated and not working with the latest version of JavaScriptKit.

Also, it turns out that `insertAdjacentHTML` does not work for script injection, although it does seem to work for styles injection. Separate `createElement`, `setAttribute`, and `appendChild` calls do seem to work for scripts.
2020-12-19 15:28:30 +00:00
David Hunt ba17b79b1d
Added some missing TokamakDOM/Core type typealiases (#331) 2020-12-10 10:42:59 +00:00
Max Desiatov 99581929a2
Add `TextEditor` implementation (#329)
* Add `TextEditor` implementation

Resolves #173.

* Clean up and bump requirements in the demo project

* Use a single `_tokamak-formcontrol` CSS class

* Add missing CSS class to `TextEditor.swift`

Co-authored-by: Jed Fox <git@jedfox.com>

Co-authored-by: Jed Fox <git@jedfox.com>
2020-12-07 21:13:24 +00:00
Max Desiatov 302cd3b108
Add `PreviewProvider` protocol (#328)
* Add `PreviewProvider` protocol

No functionality behind, just makes it easier to integrate with existing SwiftUI projects.

* Update PreviewProvider.swift

* Add missing preview-related types and modifiers
2020-12-07 17:55:20 +00:00
Max Desiatov 8230e98072
Add Discord link to `README.md` 2020-12-06 17:12:22 +00:00
Max Desiatov b4b0efca4d
Bump version to 0.6.1, update `CHANGELOG.md` 2020-12-06 14:34:12 +00:00
Max Desiatov e7f295954f
Add macOS `xcodebuild` invocation to `ci.yml` 2020-12-06 14:28:48 +00:00
Max Desiatov 37cdf6e454
Use `lowercased()` to fix Xcode autocomplete issue 2020-12-06 14:21:50 +00:00
Max Desiatov 8ad964c2f9
Mark `Link` view as implemented in `progress.md` 2020-12-06 13:56:53 +00:00
Max Desiatov abceb8609f
Update progress.md 2020-12-04 11:38:01 +00:00
Max Desiatov 2f97ecfe16
Bump version to 0.6.0, update `CHANGELOG.md` 2020-12-04 11:35:22 +00:00
Max Desiatov 1b814d0583
Add @kateinoigakukun to the list of maintainers (#310) 2020-12-04 11:20:26 +00:00
Jed Fox 797c0d849f
Add `Image` implementation, bump JSKit to 0.9.0 (#155)
* Add Image view

* Add image to demo

* Update progress.md

* Alt text

* Use Bundle type to load images, remove systemName

* Add `logo-header.png` resource to `TokamakDemo`

* Reduce image size in the demo

* Allow bundles passed to `Image` to be optional

* Pass `nil` as a default `bundle` to `Image`

Co-authored-by: Max Desiatov <max@desiatov.com>
2020-12-04 11:19:55 +00:00
Carson Katri 9d347f49f3
Add Preferences (#307)
This adds the `PreferenceKey` protocol and related modifiers.

* Initial PreferenceKey implementation

* Don't send default value to match SwiftUI behavior

* Add CustomDebugStringConvertible conformance to Color

* PR fixes

* Fix onAppear and preference modification calls

* Attempt macOS build fix

* Fix <background/overlay>PreferenceValue

* Implement/revise transformPreference

* Fix linter warnings, apply SwiftFormat

Co-authored-by: Max Desiatov <max@desiatov.com>
2020-12-04 11:19:14 +00:00
Max Desiatov 2e8e458b9c
Remove unused Dangerfile.swift (#311)
We already use SwiftLint as a separate action, and don't run Danger checks on CI anyway.
2020-11-28 14:31:51 +00:00
Max Desiatov cabe5abef5
Bump version to 0.5.3, update `CHANGELOG.md` 2020-11-28 11:39:30 +00:00
Max Desiatov dfcacc862f
Fix update of `checked` property of checkbox input (#309)
Resolve #287.

The `checked` attribute is a peculiar one, as any value on it keeps the checkbox checked. Attribute updates in `DOMRenderer` don't handle removals of attributes, but this seems to be the only case where this is relevant. I've added special handling for this attribute and checkbox inputs, and also had to declare `HTMLAttribute.checked` to set `isUpdatedAsProperty: true` on it for it to fully work.
2020-11-28 11:27:18 +00:00
Max Desiatov c754b313ef
Use latest macOS and Xcode on CI (#308)
* Use latest macOS and Xcode on CI

* Update ci.yml
2020-11-27 14:17:45 +00:00
Max Desiatov b26eb71f3e
Fix `carton dev` guidance for the demo product 2020-11-26 13:53:23 +00:00
Max Desiatov 05465be93d
Use `JSScheduler` from `OpenCombineJS` package (#304)
Now that OpenCombineJS had its first release, we can rely on its `JSScheduler` implementation.
2020-11-26 09:01:54 +00:00
Max Desiatov d5a50e7045
Bump version to 0.5.2, update CHANGELOG and README 2020-11-12 14:44:16 +00:00
Max Desiatov 3451d9ea12
Pass sibling to `Renderer.mount`, fix update order (#301)
Resolves, but adds no tests cases to the test suite for #294. See the issue for the detailed description of the problem.

I will add end-to-end tests for this in future PRs.

I've tested these cases manually so far:

```swift
struct Choice: View {
  @State private var choice = false

  var body: some View {
    HStack {
      Button("Trigger") {
        choice.toggle()
      }
      if choice {
        Group {
          Text("true")
          Text("true")
        }
      } else {
        VStack {
          Text("false")
        }
      }
      Text("end")
    }
  }
}
```

Note the `Group` view with multiple children in this one, it uncovered required checks for `GroupView` conformance.

Also tested these more simple cases:

```swift
struct Choice: View {
  @State private var choice = false

  var body: some View {
    HStack {
      Button("Trigger") {
        choice.toggle()
      }
      if choice {
        Group {
          // single child
          Text("true")
        }
      } else {
        VStack {
          Text("false")
        }
      }
      Text("end")
    }
  }
}
```

and

```swift
struct Choice: View {
  @State private var choice = false

  var body: some View {
    HStack {
      Button("Trigger") {
        choice.toggle()
      }
      if choice {
        // single child, no nesting
        Text("true")
      } else {
        VStack {
          Text("false")
        }
      }
      Text("end")
    }
  }
}
```
2020-11-11 19:34:45 +00:00
Max Desiatov 33adba20ab
Fix build after SwiftFormat changes 2020-11-09 12:47:27 +00:00
Max Desiatov fb3ab974df
Bump version to 0.5.1, update `CHANGELOG.md` 2020-11-09 12:27:26 +00:00
Max Desiatov f24a09f006
Apply latest SwiftFormat 2020-11-09 12:27:17 +00:00
Max Desiatov bff7c1bf27
Remove unused `.swift-version` file 2020-11-09 12:25:04 +00:00
Yuta Saito c813061b17
Update Package.resolved (#300) 2020-11-09 12:20:41 +00:00
Max Desiatov 7320de9857
Allow use of Combine to enable Xcode autocomplete (#299)
It looks weird, but it works 🤷‍♂️
2020-11-09 12:20:22 +00:00
Max Desiatov 082fa19398
Fix linter warning 2020-11-09 12:02:25 +00:00
Max Desiatov 18da2d279e
Fix typo in CHANGELOG.md 2020-11-08 23:47:03 +00:00
Max Desiatov 014383f751
Bump version to 0.5.0, update `CHANGELOG.md` (#298)
* Bump version to 0.5.0, update `CHANGELOG.md`

* Update CHANGELOG.md

Co-authored-by: Carson Katri <Carson.katri@gmail.com>

Co-authored-by: Carson Katri <Carson.katri@gmail.com>
2020-11-08 23:22:36 +00:00
Yuta Saito af08c1a6f6
Xcode compatibility (#297)
* Ignore xcodeproj generated by SwiftPM

* Update to use official OpenCombine to avoid Xcode build error

* Use forked version with ObservableObject implementation

* Fix ambigious error

* Ignore SwiftPM edit mode package

* Update toolchain version
2020-11-08 19:42:25 +09:00
Max Desiatov 9681b91a84
Allow tests to be run on macOS (#295)
Requires #276 to be merged, as earlier versions of JavaScriptKit can't be depended on in macOS builds due to unsafe flags.

* Add Link View

* Add Publish support

* Remove #if checks

* Upgrade swift snapshot

* Try swiftwasm-action@main

* Remove Publish support from this repo

* Remove TokamakPublish target

* Allow tests to be run on macOS

* Update `ci.yml` to build and run the test product

* Trigger CI on all PRs without branch restrictions

* Rename linux_build to swiftwasm_build in ci.yml

Co-authored-by: Carson Katri <carson.katri@gmail.com>
2020-11-07 10:11:35 +00:00
Carson Katri 348408eba1
Add Link view, update JavaScriptKit to 0.8.0 (#276)
* Add Link View

* Add Publish support

* Remove #if checks

* Upgrade swift snapshot

* Try swiftwasm-action@main

* Remove Publish support from this repo

* Remove TokamakPublish target
2020-11-05 16:37:56 -05:00
Carson Katri a5da04989e
Add `AnyColorBox` and `AnyFontBox` (#291)
* Add AnyColorBox implementation

* Add TokenDeferredToRenderer

* Implement AnyFontBox

* Add Any[Color/Font]BoxDeferredToRenderer

* Resolve linter errors

* Appease the linter
2020-10-28 18:26:42 -04:00
Max Desiatov 94dc934fe4
Replace Danger with SwiftLint to improve warnings (#293)
Looks like the Danger action duplicates warning comments. The new SwiftLint action does not, although warnings only show up in the diff view, which I think is an acceptable trade-off.
2020-10-27 21:43:42 +00:00
Max Desiatov 1e43d98bb2
Use v5.3 tag of `swiftwasm-action` in `ci.yml` (#292)
`swiftwasm-action@master` no longer exists, and we should use a tag to rely on more stable action code anyway.
2020-10-25 12:40:48 +00:00
Max Desiatov af225afab7
Add @carson-katri and @kateinoigakukun to `FUNDING.yml` (#289)
* Add @carson-katri and @kateinoigakukun to `FUNDING.yml`

* Update README.md
2020-10-07 08:07:45 +01:00
Max Desiatov dbd1ee46c4
Add `URLHashDemo` w/ `window.onhashchange` closure (#288)
* Add `URLHashDemo` w/ `window.onhashchange` closure

Resolves #284

* Assign `.undefined` in HashState.deinit
2020-10-06 21:18:18 +01:00
Max Desiatov a631d181e6
Update required `carton` version in `README.md` 2020-10-04 18:16:23 +01:00
Max Desiatov b5b68c4186
Update `CHANGELOG.md` for 0.4.0 release 2020-09-30 11:22:45 +01:00
Max Desiatov ee0006a6a3
Fix compatibility with JavaScriptKit 0.7 (#281)
This PR requires `carton` 0.6.0 that you can install from Homebrew as usual.

To cleanly manage scheduler closures, new `JSScheduler` class is introduced that conforms to OpenCombine's `Scheduler` protocol. I think it will be moved to OpenCombineJS in the future.

* Fix compatibility with JavaScriptKit 0.7

* Formatting update

* Specify `carton` 0.6.0 as a requirement

* Optimize immediate schedule function

* Update formatting
2020-09-30 10:17:19 +01:00
Yuta Saito 278201cbd3
Re-export HTML in TokamakDOM (#275) 2020-09-24 00:26:01 +09:00
Max Desiatov de72316efa
Use setAttribute, not properties to fix SVG update (#279)
Property assignment does not update SVG elements. We may want to use properties instead of `setAttribute` in the future for optimizations. For now I think that `setAttribute` works in all cases, including SVG, and seems safer to me.

Resolves #278.

* Use setAttribute, not properties to fix SVG update

* Add HTMLAttribute to cleanly apply DOM updates

* Add doc comment to HTMLAttribute

* Update Sources/TokamakStaticHTML/Views/HTML.swift

Co-authored-by: Jed Fox <git@jedfox.com>

* Use static func property in TokamakDOM/TextField.swift

* Make `static func property` public

* Declare `static let value` on `HTMLAttribute`

Co-authored-by: Jed Fox <git@jedfox.com>
2020-09-23 09:13:22 +01:00
Yuta Saito ba7af1d014
Allow non-body mount host node (#271) 2020-08-31 08:38:55 -04:00
Jed Fox 5141bee7d7
Fix the sizing of sliders (#268) 2020-08-26 22:29:21 -04:00
Jed Fox 523c53f14a
Add `Slider` implementation (#228)
* Slider MVP

* Update progress.md

* Update Slider.swift

* Update SliderDemo.swift

* Allow any BinaryFloatingPoint

* Add Mac Catalyst Tokamak demo

* Add basic onEditingChanged support

This likely has bugs if you touch down multiple fingers on the slider then lift one finger.

* Demo improvements

* Update ScrollView to match SwiftUI

This isn’t documented but it visually appears to wrap the content in a VStack

* Restyle the sliders

* Update Slider.swift

* Make convert functions private

* Fix line length

* Update progress.md and FIXMEs

* Wrap comments
2020-08-26 13:33:06 -04:00
Max Desiatov 5f450d0e38
Fix typo in 0.3.0 entry in `CHANGELOG.md` 2020-08-20 15:50:03 +01:00
Max Desiatov 8ce22014dd
Update 0.3.0 entry in `CHANGELOG.md` 2020-08-20 12:41:21 +01:00
Max Desiatov 6affca5931
Specify `carton` 0.5.0 version as a requirement in `README.md` 2020-08-20 12:02:12 +01:00
Max Desiatov 0fe81a060d
Remove Xcode 12 warning from README.md (#264)
The latest 5.3 snapshots are compatible with Xcode 12, so the warning is no longer needed.
2020-08-20 12:01:17 +01:00
Max Desiatov ca5d4fc4ac
Add missing JavaScriptKit import to `README.md` (#265)
The import is currently missing, which may be confusing, especially if example code is copied without any context.
2020-08-20 00:30:12 +01:00
Max Desiatov 75d178e5dd
Update `CHANGELOG.md` for 0.3.0 release 2020-08-19 10:11:34 +01:00
Max Desiatov 82111c54a0
Set versions of dependencies in `Package.swift` (#262)
This allows specifying 0.3 (to be released after this PR is merged) dependency on Tokamak in `carton` templates, otherwise branch/commit dependencies in our `Package.swift` can't be correctly resolved.

`swift test` is temporarily disabled on macOS as the upstream Swift toolchain doesn't support unsafe flags in the JavaScriptKit dependency together with a `from` constraint. We could run it on Linux, but my OpenCombine fork doesn't support Linux builds yet (logged as #263).
2020-08-19 10:08:00 +01:00
Max Desiatov d97a5f3215
Update `CHANGELOG.md` for 0.3.0 release 2020-08-17 18:31:31 +01:00
Max Desiatov 2383a17c2d
Implement `StateObject` property wrapper (#260)
Resolves #158.

Fixes a bug where `NavigationView` destination was reset after scene phase changes (or any re-renders caused by environment changes for that matter).

This was caused by `@ObservedObject` destination being recreated, now `@StateObject` persists it across re-renders.

The `setter` property of the `ValueStorage` protocol is now moved to a separate `WritableValueStorage` protocol. The reasoning is that `StateObject` doesn't need its wrapped value to be set directly as it operates on it by reference, not by value, thus `StateObject` doesn't need any wrapped value setters.
2020-08-17 16:01:49 +01:00
Max Desiatov b9f5ef07ab
Fix `NavigationView` broken state after re-render (#259) 2020-08-15 18:07:19 +01:00
Max Desiatov b7434a2e54
Add `GeometryReader` implementation (#239)
This is just an empty API at the moment. I hope it can be implemented purely in the `deferredBody` of `GeometryReader` with [the ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) without requiring any tweaks in the `Renderer` protocol or the reconciler.

Seems like I need the `domRef` modifier that writes `JSObjectRef` to a given binding working first, as discussed in #231.
2020-08-11 16:47:12 +01:00
Carson Katri c43d2db1b3
Add default dark styles for Views (#241) 2020-08-10 16:05:53 -04:00
Max Desiatov 2a49b7808b
Link to the renderers guide from `README.md` (#251)
* Link to the renderers guide from `README.md`

The guide itself was merged into a single file for easier navigation.

* Update RenderersGuide.md
2020-08-07 16:01:27 +01:00
Max Desiatov e11effdd8c
Use the latest 5.3 snapshot in `.swift-version` (#252)
* Use the latest 5.3 snapshot in `.swift-version`

These SwiftWasm snapshots should be more stable in general and also have a workaround for https://github.com/swiftwasm/JavaScriptKit/issues/6 included. They still use the old metadata layout, so Runtime and OpenCombine dependencies had to be updated in `Package.swift` for `@ObservableObject` to work with these snapshots.

* Fix linter warning
2020-08-06 13:57:36 +01:00
Max Desiatov de37894f83
Fix color scheme observer crashes in Safari (#249)
Resolve #245. Turns out `matchMediaDarkScheme` object doesn't have `addEventListener`, but only `addListener` in Safari 13.1.2.
2020-08-05 17:30:50 +01:00
Max Desiatov c4c9eb595e
Update to the latest version of SwiftFormat (#250)
* Update to the latest version of SwiftFormat

This fixes inconsistencies in argument and parameter formatting that we previously had.

* Fix function length in `Path.swift`

* Fix linter warnings

* More formatting cleanups

* Add `StrokeStyle.zero` in the `StaticHTML` module
2020-08-05 17:30:19 +01:00
Jed Fox 88064fd5bc
Split demo list into sections (#243)
* Split demo list into sections

* Remove slider demo

* Update RedactDemo.swift

* Redact → Redaction, move DOM refs to a TokamakDOM section, additional adjustments

* RedactDemo → RedactionDemo
2020-08-04 06:49:43 -04:00
Max Desiatov 62c05ae9aa
Remove some `AnyView` in the `List` implementation (#246) 2020-08-04 07:38:27 +01:00
Max Desiatov b93be40a19
Add `_targetRef` and `_domRef` modifiers (#240)
Resolves partially #231. `_targetRef` is a modifier that can be used by any renderer, while `_domRef` is an adaptation of that for `DOMRenderer`. Both are underscored as they are not available in SwiftUI, and also their stability is currently not so well known to us, we may consider changing this API in the future.

Example use:

```swift
struct DOMRefDemo: View {
  @State var button: JSObjectRef?

  var body: some View {
    Button("Click me") {
      button?.innerHTML = "This text was set directly through a DOM reference"
    }._domRef($button)
  }
}
```

I've also fixed all known line length warnings in this PR.
2020-08-02 22:01:38 +01:00
Max Desiatov c7b5e75e1a
Add `ColorScheme` environment (#136)
You can see the dark scheme environment text representation updated in `EnvironmentDemo`. I suggest adding default dark mode styles in a separate PR, I've created #237 as a reminder for that.
2020-08-02 18:55:35 +01:00
Carson Katri 70d31b2e5b
Add `redacted` modifier (#232) 2020-08-01 17:18:23 -04:00
Carson Katri 4c654da456
Add Static HTML Renderer and Documentation (#204) 2020-08-01 16:27:12 -04:00
Max Desiatov fbb893739b
Fix tests, move DefaultButtonStyle to TokamakCore (#234)
Merging #214 broke the tests. Also, `DefaultButtonStyle` seems to be trivial enough to be shared through `TokamakCore` between all renderers.
2020-08-01 20:07:10 +01:00
Max Desiatov 050e917161
Add `test-suite` label to `label.yml` 2020-08-01 19:41:25 +01:00
Max Desiatov 40804d4542
Remove `DefaultApp`, make `DOMRenderer` internal (#227)
Removes the `View`-based initializer of `DOMRenderer` which no longer leaves any `public` initializers on it, means we can make it fully internal. `DOMNode` is now internal too, which is great as it was an implementation detail anyway. Corollary, `DefaultApp` is no longer needed.

`Target` was cleaned up is it doesn't need to hold `App` or `Scene` values, now it's just a simple protocol.

I've updated `README.md` to show usage of the `App` protocol in the basic example.

Closes #224.
2020-08-01 18:46:59 +01:00
Max Desiatov e37d13017c
Add basic `ButtonStyle` implementation (#214)
This based off the `buttonstyles` branch by @Outcue.

Initially it didn't work because mounted host views didn't propagate their environment on updates. This is now fixed by adding `updateEnvironment` function on `MountedElement` base class and calling it in the initializer. Manual environment updates are no longer needed in `makeMounted...` factory functions. `makeMountedApp` is no longer needed at all and `MountedApp` initializer can be used directly then.
2020-08-01 18:46:34 +01:00
483 changed files with 37010 additions and 4218 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,6 +1,6 @@
# These are supported funding model platforms
github: MaxDesiatov # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: [carson-katri, kateinoigakukun, MaxDesiatov] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

44
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,44 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: carson-katri
---
<!-- Replace the placeholders below with details that help us to reproduce the bug. -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
<!-- If this is a layout/rendering issue, please provide a self-contained code snippet that reproduces it. -->
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
**Desktop (please complete the following information):**
- OS: [e.g. macOS 12.4]
- Browser [e.g. chrome, safari]
- Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.10.1]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone 6]
- OS: [e.g. iOS15.1]
- Browser [e.g. stock browser, safari]
- Version of the browser [e.g. 22]
- Version of Tokamak [e.g. 0.10.1]
**Additional context**
Add any other context about the problem here.

View File

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

33
.github/workflows/codecov.yml vendored Normal file
View File

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

View File

@ -1,23 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: Danger
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
pull_request:
branches: [main]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
danger-lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Danger Swift
uses: maxdesiatov/danger-swift@swiftlint-docker
if: github.event.pull_request.head.repo.full_name == github.repository
env:
GITHUB_TOKEN: ${{ secrets.PAT }}

View File

@ -11,7 +11,7 @@ on:
jobs:
check-labels:
# The type of runner that the job will run on
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Match PR Label
@ -24,5 +24,8 @@ jobs:
dependencies,
documentation,
enhancement,
Fiber,
refactor,
SwiftUI compatibility,
test suite,
GTK renderer,

24
.github/workflows/swiftlint.yml vendored Normal file
View File

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

5
.gitignore vendored
View File

@ -19,6 +19,7 @@ DerivedData
*.hmap
*.ipa
.swiftpm/xcode
*.xcodeproj
# Bundler
.bundle
@ -39,3 +40,7 @@ Pods/
# SwiftPM
.build
/Packages
# VS Code
.vscode/launch.json

View File

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

View File

@ -1 +1 @@
wasm-DEVELOPMENT-SNAPSHOT-2020-06-12-a
wasm-5.6.0-RELEASE

View File

@ -1,9 +1,14 @@
--indent 2
--indentcase false
--trimwhitespace always
--empty tuple
--voidtype tuple
--nospaceoperators ..<,...
--ifdef noindent
--stripunusedargs closure-only
--maxwidth 100
--wraparguments before-first
--funcattributes prev-line
--typeattributes prev-line
--varattributes prev-line
--disable andOperator
--swiftversion 5.2
--swiftversion 5.6

View File

@ -9,6 +9,7 @@ disabled_rules:
- type_name
- todo
- large_tuple
- opening_brace
line_length: 100

View File

@ -1,4 +1,7 @@
{
"editor.formatOnSave": true,
"licenser.author": "Tokamak contributors"
"licenser.author": "Tokamak contributors",
"cSpell.words": [
"Tokamak"
]
}

27
.vscode/tasks.json vendored
View File

@ -11,12 +11,37 @@
{
"label": "swift test",
"type": "shell",
"command": "swift test"
"command": "swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest"
},
{
"label": "carton dev",
"type": "shell",
"command": "carton dev --product TokamakDemo"
},
{
"label": "benchmark",
"type": "shell",
"command": "./benchmark.sh"
},
{
"label": "make",
"type": "shell",
"command": "make",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "make run",
"type": "shell",
"command": "make run",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@ -1,4 +1,438 @@
# 0.2.0 (21 July, 2020)
# 0.10.1 (20 May 2022)
This is a small bugfix release, which updates JavaScriptKit dependency to 0.15 and required version of `carton` to 0.15.
**Merged pull requests:**
- Update JSKit dependency ([#482](https://github.com/TokamakUI/Tokamak/pull/482)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Explicitly mention `carton` version in "Requirements" ([#481](https://github.com/TokamakUI/Tokamak/pull/481)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use stable `v5.6` version of `swiftwasm-action` ([#477](https://github.com/TokamakUI/Tokamak/pull/477)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.10.0 (9 April 2022)
This release adds support for SwiftWasm 5.6. It also updates JavaScriptKit and OpenCombineJS dependencies.
Due to issues with support for older SwiftWasm releases in the `carton`/SwiftPM integration, Tokamak now requires
SwiftWasm 5.6 or later, while SwiftWasm 5.4 and 5.5 are no longer supported.
**Merged pull requests:**
- Build and test with SwiftWasm 5.6 on CI ([#475](https://github.com/TokamakUI/Tokamak/pull/475)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.9.1 (16 February 2022)
This release fixes an issue with `EnvironmentValues`, updates CI workflow for SwiftWasm 5.5, and bumps JavaScriptKit dependency to 0.12.0.
**Merged pull requests:**
- Fix typo ([#462](https://github.com/TokamakUI/Tokamak/pull/462)) via [@regexident](https://github.com/regexident)
- Fix `rootEnvironment` not merged with `.defaultEnvironment` ([#461](https://github.com/TokamakUI/Tokamak/pull/461)) via [@regexident](https://github.com/regexident)
- Build and test with SwiftWasm 5.5 on CI ([#460](https://github.com/TokamakUI/Tokamak/pull/460)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.9.0 (26 November 2021)
This release adds support for SwiftWasm 5.5 and bumps the minimum required version to Swift 5.4.
It now depends on JavaScriptKit 0.11.1, which no longer requires manual memory management of
`JSClosure` instances. The downside of that update is that minimum browser version requirements are
significantly higher now. See [`README.md`](README.md#requirements) for more details.
Additionally, a few new features were added to the DOM renderer:
- `Canvas` and `TimelineView`;
- `onHover` modifier;
- `task` modifier for running `async` functions;
- Sanitizers for `Text` view.
Many thanks (in alphabetical order) to [@agg23](https://github.com/agg23),
[@carson-katri](https://github.com/carson-katri), [@ezraberch](https://github.com/ezraberch),
and [@mbrandonw](https://github.com/mbrandonw) for their contributions to this release!
**Closed issues:**
- `TextField` Not Rendering the field ([#455](https://github.com/TokamakUI/Tokamak/issues/455))
- Can't find `CGSize` or `CGFloat` type ([#450](https://github.com/TokamakUI/Tokamak/issues/450))
- `UnitPoint` constants don't match SwiftUI ([#443](https://github.com/TokamakUI/Tokamak/issues/443))
**Merged pull requests:**
- Update for JSKit 0.11.1, add async `task` modifier ([#457](https://github.com/TokamakUI/Tokamak/pull/457)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Switch to Xcode 13.0 in `gtk_macos_build` job ([#454](https://github.com/TokamakUI/Tokamak/pull/454)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `Canvas` and `TimelineView` to DOM renderer ([#449](https://github.com/TokamakUI/Tokamak/pull/449)) via [@carson-katri](https://github.com/carson-katri)
- Initial implementation of `onHover` ([#448](https://github.com/TokamakUI/Tokamak/pull/448)) via [@agg23](https://github.com/agg23)
- Refactor `NavigationView` ([#446](https://github.com/TokamakUI/Tokamak/pull/446)) via [@ezraberch](https://github.com/ezraberch)
- Save HTML snapshots with .html extension. ([#447](https://github.com/TokamakUI/Tokamak/pull/447)) via [@mbrandonw](https://github.com/mbrandonw)
- Add HTML renderer support for AngularGradient ([#444](https://github.com/TokamakUI/Tokamak/pull/444)) via [@ezraberch](https://github.com/ezraberch)
- Bump requirements to Swift 5.4, migrate to `@resultBuilder` ([#442](https://github.com/TokamakUI/Tokamak/pull/442)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add HTML sanitizer to `Text` ([#437](https://github.com/TokamakUI/Tokamak/pull/437)) via [@ezraberch](https://github.com/ezraberch)
- Add `@ezraberch` to the list of maintainers ([#440](https://github.com/TokamakUI/Tokamak/pull/440)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.8.0 (17 August 2021)
This release adds support for more SwiftUI types and modifiers, and fixes bugs. Including, but not
limited to:
- `Toolbar` type and `toolbar` modifier
- `ProgressView` type
- `Animation` and related types and modifiers
- `opacity`, `scaleEffect`, `aspectRatio`, and `controlSize` modifiers
- `Material` and `Gradient` types
- `HierarchicalShapeStyle` (`.primary`/`.secondary`/`.tertiary`/`.quaternary`) type
- `ContainerRelativeShape` type
- `spacing` argument support for initializers of `HStack` and `VStack`
- support for standard Foundation types, such as `CGRect`, `CGSize` (we previously used our own
implementation of those, which weren't fully compatible with Foundation)
- ability to sort HTML attributes when generating static HTML, which is essential for end-to-end
tests that cover generated output.
Many thanks to [@carson-katri](https://github.com/carson-katri),
[@ezraberch](https://github.com/ezraberch), and [@yonihemi](https://github.com/yonihemi) for
their contributions to this release!
**Closed issues:**
- Is there anyway to compile this from Xcode? ([#406](https://github.com/TokamakUI/Tokamak/issues/406))
- Xcode doesn't compile — gtk/gtk.h not found ([#405](https://github.com/TokamakUI/Tokamak/issues/405))
- Use `NSGeometry` types from Foundation ([#404](https://github.com/TokamakUI/Tokamak/issues/404))
- Adding padding to a view contained in a Button causes the Button to disappear ([#403](https://github.com/TokamakUI/Tokamak/issues/403))
- .background modifier with contained shape causes view to expand to full vertical size of the screen ([#402](https://github.com/TokamakUI/Tokamak/issues/402))
- Multi-line string handling in Text views ([#400](https://github.com/TokamakUI/Tokamak/issues/400))
- Content with spacer jumps when blurring and focusing the page ([#395](https://github.com/TokamakUI/Tokamak/issues/395))
- Frame sizes do not match expected behavior. ([#387](https://github.com/TokamakUI/Tokamak/issues/387))
- URL hash change demo crashes ([#369](https://github.com/TokamakUI/Tokamak/issues/369))
- Infinite loops w/ 100% CPU usage caused by stack overflows ([#367](https://github.com/TokamakUI/Tokamak/issues/367))
- TokamakDemo breaks after use of `_domRef` ([#326](https://github.com/TokamakUI/Tokamak/issues/326))
- Add support for `toolbar` modifier and related types ([#316](https://github.com/TokamakUI/Tokamak/issues/316))
**Merged pull requests:**
- Revise `ShapeStyle` and add `Gradient`s ([#435](https://github.com/TokamakUI/Tokamak/pull/435)) via [@carson-katri](https://github.com/carson-katri)
- Add `Toolbar` implementation for HTML renderer ([#169](https://github.com/TokamakUI/Tokamak/pull/169)) via [@carson-katri](https://github.com/carson-katri)
- Fix SwiftLint action ([#434](https://github.com/TokamakUI/Tokamak/pull/434)) via [@ezraberch](https://github.com/ezraberch)
- Add View Traits and transitions ([#426](https://github.com/TokamakUI/Tokamak/pull/426)) via [@carson-katri](https://github.com/carson-katri)
- Add `ToolbarItem` and its builder functions ([#430](https://github.com/TokamakUI/Tokamak/pull/430)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `controlSize`/`controlProminence` modifiers ([#431](https://github.com/TokamakUI/Tokamak/pull/431)) via [@carson-katri](https://github.com/carson-katri)
- Fix background/overlay layout in DOM/HTML renderers ([#429](https://github.com/TokamakUI/Tokamak/pull/429)) via [@carson-katri](https://github.com/carson-katri)
- Add `ProgressView` ([#425](https://github.com/TokamakUI/Tokamak/pull/425)) via [@carson-katri](https://github.com/carson-katri)
- Add support for custom fonts ([#421](https://github.com/TokamakUI/Tokamak/pull/421)) via [@carson-katri](https://github.com/carson-katri)
- Animation implementation using the Web Animations API ([#427](https://github.com/TokamakUI/Tokamak/pull/427)) via [@carson-katri](https://github.com/carson-katri)
- Add `scaleEffect` modifier ([#424](https://github.com/TokamakUI/Tokamak/pull/424)) via [@carson-katri](https://github.com/carson-katri)
- Add `aspectRatio` modifier ([#422](https://github.com/TokamakUI/Tokamak/pull/422)) via [@carson-katri](https://github.com/carson-katri)
- Check minWidth/Height == nil ([#420](https://github.com/TokamakUI/Tokamak/pull/420)) via [@carson-katri](https://github.com/carson-katri)
- Add Primary/Secondary/Tertiary/QuaternaryContentStyle ([#419](https://github.com/TokamakUI/Tokamak/pull/419)) via [@carson-katri](https://github.com/carson-katri)
- Add `Material` to the HTML renderer ([#418](https://github.com/TokamakUI/Tokamak/pull/418)) via [@carson-katri](https://github.com/carson-katri)
- Improve ShapeStyles to match iOS 15+ ([#417](https://github.com/TokamakUI/Tokamak/pull/417)) via [@carson-katri](https://github.com/carson-katri)
- Add ContainerRelativeShape ([#416](https://github.com/TokamakUI/Tokamak/pull/416)) via [@carson-katri](https://github.com/carson-katri)
- Add HTML implementation for `opacity` modifier ([#415](https://github.com/TokamakUI/Tokamak/pull/415)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Support `spacing` property on `HStack`/`VStack` ([#273](https://github.com/TokamakUI/Tokamak/pull/273)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Explicitly import CoreFoundation ([#413](https://github.com/TokamakUI/Tokamak/pull/413)) via [@yonihemi](https://github.com/yonihemi)
- Fix handling of stroked shapes ([#414](https://github.com/TokamakUI/Tokamak/pull/414)) via [@ezraberch](https://github.com/ezraberch)
- Add a snapshot test for `Path` SVG layout ([#412](https://github.com/TokamakUI/Tokamak/pull/412)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Attempt `padding` modifier fusion to avoid nested `div`s ([#253](https://github.com/TokamakUI/Tokamak/pull/253)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use `CGFloat`, `CGPoint`, `CGRect` from Foundation ([#411](https://github.com/TokamakUI/Tokamak/pull/411)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add reconciler stress tests for elaborate testing ([#381](https://github.com/TokamakUI/Tokamak/pull/381)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix spacers after `DOMRenderer.update` ([#410](https://github.com/TokamakUI/Tokamak/pull/410)) via [@ezraberch](https://github.com/ezraberch)
- Replace `ViewDeferredToRenderer`, fix renderer tests ([#408](https://github.com/TokamakUI/Tokamak/pull/408)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Allow DOMRenderer to render buttons with non-Text labels (#403) ([#409](https://github.com/TokamakUI/Tokamak/pull/409)) via [@ezraberch](https://github.com/ezraberch)
- Sort attributes in HTML nodes when rendering ([#346](https://github.com/TokamakUI/Tokamak/pull/346)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.7.0 (3 May 2021)
This release introduces new view types such as `DatePicker`, new modifiers such as `shadow`,
improves test coverage, updates dependencies, and fixes multiple bugs and crashes. Additionally,
a proof of concept GTK renderer is now available in the `TokamakGTK` module.
Many thanks to (in alphabetical order)
[@carson-katri](https://github.com/carson-katri), [@filip-sakel](https://github.com/filip-sakel),
[@foscomputerservices](https://github.com/foscomputerservices), [@literalpie](https://github.com/literalpie),
[@mattpolzin](https://github.com/mattpolzin), [@mortenbekditlevsen](https://github.com/mortenbekditlevsen),
and [@Snowy1803](https://github.com/Snowy1803) for their contributions to this release!
**Closed issues:**
- `@ObservedObject` is a get-only property ([#392](https://github.com/TokamakUI/Tokamak/issues/392))
- What is the difference between `HTML` and `DynamicHTML`? ([#388](https://github.com/TokamakUI/Tokamak/issues/388))
- Reduce `View.body` Visibility ([#385](https://github.com/TokamakUI/Tokamak/issues/385))
- Verify that type constructor names contain contain module names ([#368](https://github.com/TokamakUI/Tokamak/issues/368))
- Crash when using a `View` with optional content ([#362](https://github.com/TokamakUI/Tokamak/issues/362))
- Set up code coverage reports on GitHub Actions ([#350](https://github.com/TokamakUI/Tokamak/issues/350))
- Shadow support ([#324](https://github.com/TokamakUI/Tokamak/issues/324))
- Implement `DatePicker` view in the DOM renderer ([#320](https://github.com/TokamakUI/Tokamak/issues/320))
- `TokamakDemo` build failed ([#305](https://github.com/TokamakUI/Tokamak/issues/305))
**Merged pull requests:**
- Add `@dynamicMemberLookup` to `Binding` ([#396](https://github.com/TokamakUI/Tokamak/pull/396)) via [@carson-katri](https://github.com/carson-katri)
- Add `DatePicker` to the `TokamakDOM` module ([#394](https://github.com/TokamakUI/Tokamak/pull/394)) via [@Snowy1803](https://github.com/Snowy1803)
- Use `String(reflecting:)` vs `String(describing:)` ([#391](https://github.com/TokamakUI/Tokamak/pull/391)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Clarify the difference between `HTML` and `DynamicHTML` ([#389](https://github.com/TokamakUI/Tokamak/pull/389)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `_spi(TokamakCore)` to ideally internal public members ([#386](https://github.com/TokamakUI/Tokamak/pull/386)) via [@filip-sakel](https://github.com/filip-sakel)
- Make properties of `CGPoint`, `CGSize` and `CGRect` `var`s instead of `let`s ([#382](https://github.com/TokamakUI/Tokamak/pull/382)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Use immediate scheduler in `TestRenderer` ([#380](https://github.com/TokamakUI/Tokamak/pull/380)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Simple Code Coverage analysis ([#378](https://github.com/TokamakUI/Tokamak/pull/378)) via [@mattpolzin](https://github.com/mattpolzin)
- Add checks for metadata state ([#375](https://github.com/TokamakUI/Tokamak/pull/375)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use upstream OpenCombine instead of a fork ([#377](https://github.com/TokamakUI/Tokamak/pull/377)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Update JavaScriptKit, OpenCombineJS dependencies ([#376](https://github.com/TokamakUI/Tokamak/pull/376)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Clean up metadata reflection code ([#372](https://github.com/TokamakUI/Tokamak/pull/372)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add David Hunt to the list of maintainers ([#373](https://github.com/TokamakUI/Tokamak/pull/373)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Refactor environment injection, add a test ([#371](https://github.com/TokamakUI/Tokamak/pull/371)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Replace uses of the Runtime library with stdlib ([#370](https://github.com/TokamakUI/Tokamak/pull/370)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use `macos-latest` agent for the GTK build ([#360](https://github.com/TokamakUI/Tokamak/pull/360)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add a benchmark target and a script to run it ([#365](https://github.com/TokamakUI/Tokamak/pull/365)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix crashes in views with optional content ([#364](https://github.com/TokamakUI/Tokamak/pull/364)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add GTK support for `SecureField` ([#363](https://github.com/TokamakUI/Tokamak/pull/363)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Add support for shadow modifier ([#355](https://github.com/TokamakUI/Tokamak/pull/355)) via [@literalpie](https://github.com/literalpie)
- Two infinite loop fixes ([#359](https://github.com/TokamakUI/Tokamak/pull/359)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Added `TextField` support for GTK using `GtkEntry` ([#361](https://github.com/TokamakUI/Tokamak/pull/361)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Fixed a small issue with re-renderers being dropped ([#356](https://github.com/TokamakUI/Tokamak/pull/356)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Removed an extra space that cause Safari to issue "Invalid value" ([#358](https://github.com/TokamakUI/Tokamak/pull/358)) via [@foscomputerservices](https://github.com/foscomputerservices)
- Add `@mortenbekditlevsen` to the list of maintainers in `README.md` ([#352](https://github.com/TokamakUI/Tokamak/pull/352)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Build the GTK renderer on Ubuntu on CI ([#347](https://github.com/TokamakUI/Tokamak/pull/347)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add missing `Link` re-export to TokamakDOM ([#351](https://github.com/TokamakUI/Tokamak/pull/351)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- GTK shape support WIP ([#348](https://github.com/TokamakUI/Tokamak/pull/348)) via [@mortenbekditlevsen](https://github.com/mortenbekditlevsen)
- Add a "bug report" issue template ([#349](https://github.com/TokamakUI/Tokamak/pull/349)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.6.1 (6 December 2020)
This release fixes autocomplete in Xcode for projects that depend on Tokamak.
# 0.6.0 (4 December 2020)
This release introduces support for the `Image` view, which can load images bundled as SwiftPM
resources. It also adds the `PreferenceKey` protocol and `preference(key:value:)`,
`onPreferenceChange`, `backgroundPreferenceValue`, `transformPreference`, and
`overlayPreferenceValue` modifiers. Many thanks to [@carson-katri](https://github.com/carson-katri)
and [@j-f1](https://github.com/j-f1) for implementing this!
**Merged pull requests:**
- Add [@kateinoigakukun](https://github.com/kateinoigakukun) to the list of maintainers ([#310](https://github.com/TokamakUI/Tokamak/pull/310)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `Image` implementation, bump JSKit to 0.9.0 ([#155](https://github.com/TokamakUI/Tokamak/pull/155)) via [@j-f1](https://github.com/j-f1)
- Add Preferences ([#307](https://github.com/TokamakUI/Tokamak/pull/307)) via [@carson-katri](https://github.com/carson-katri)
- Remove unused Dangerfile.swift ([#311](https://github.com/TokamakUI/Tokamak/pull/311)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.5.3 (28 November 2020)
A bugfix release that fixes `Toggle` values not updated when reset from a binding. Additionally, the
embedded internal implementation of `JSScheduler` is replaced with one from
[`OpenCombineJS`](https://github.com/swiftwasm/OpenCombineJS). This library is a new dependency of
Tokamak used in the DOM renderer.
**Closed issues:**
- `Toggle` value not updated when it's reset from a binding ([#287](https://github.com/TokamakUI/Tokamak/issues/287))
**Merged pull requests:**
- Fix update of `checked` property of checkbox input ([#309](https://github.com/TokamakUI/Tokamak/pull/309)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use latest macOS and Xcode on CI ([#308](https://github.com/TokamakUI/Tokamak/pull/308)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use `JSScheduler` from `OpenCombineJS` package ([#304](https://github.com/TokamakUI/Tokamak/pull/304)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.5.2 (12 November 2020)
This is a bugfix release that fixes in-tree updates in cases where type of a view changes with
conditional updates. Thanks to [@vi4m](https://github.com/vi4m) for reporting the issue!
**Merged pull requests:**
- Pass sibling to `Renderer.mount`, fix update order ([#301](https://github.com/TokamakUI/Tokamak/pull/301)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.5.1 (9 November 2020)
A bugfix release to improve compatibility with Xcode autocomplete.
**Merged pull requests:**
- Update Package.resolved ([#300](https://github.com/TokamakUI/Tokamak/pull/300)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
- Allow use of Combine to enable Xcode autocomplete ([#299](https://github.com/TokamakUI/Tokamak/pull/299)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.5.0 (9 November 2020)
This is a compatibility release with small feature additions. Namely the `Link` view is now available,
and our JavaScriptKit dependency has been updated. The latter change now allows you to open
`Package.swift` package manifests of your Tokamak projects with working auto-complete in Xcode.
Also, our dark mode implementation now more closely follows SwiftUI behavior.
Many thanks to [@carson-katri](https://github.com/carson-katri) and
[@kateinoigakukun](https://github.com/kateinoigakukun) for their contributions to this release!
**Closed issues:**
- Can't build Tokamak project - carton dev command ([#296](https://github.com/TokamakUI/Tokamak/issues/296))
- Colors should change depending on light/dark color scheme ([#290](https://github.com/TokamakUI/Tokamak/issues/290))
- Pattern for handling global dom events ([#284](https://github.com/TokamakUI/Tokamak/issues/284))
- 0.4.0 upgrade / regression? ([#283](https://github.com/TokamakUI/Tokamak/issues/283))
**Merged pull requests:**
- Xcode compatibility ([#297](https://github.com/TokamakUI/Tokamak/pull/297)) via [@kateinoigakukun](https://github.com/kateinoigakukun)
- Allow tests to be run on macOS ([#295](https://github.com/TokamakUI/Tokamak/pull/295)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add Link view, update JavaScriptKit to 0.8.0 ([#276](https://github.com/TokamakUI/Tokamak/pull/276)) via [@carson-katri](https://github.com/carson-katri)
- Add `AnyColorBox` and `AnyFontBox` ([#291](https://github.com/TokamakUI/Tokamak/pull/291)) via [@carson-katri](https://github.com/carson-katri)
- Replace Danger with SwiftLint to improve warnings ([#293](https://github.com/TokamakUI/Tokamak/pull/293)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Use v5.3 tag of `swiftwasm-action` in `ci.yml` ([#292](https://github.com/TokamakUI/Tokamak/pull/292)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add @carson-katri and @kateinoigakukun to `FUNDING.yml` ([#289](https://github.com/TokamakUI/Tokamak/pull/289)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `URLHashDemo` w/ `window.onhashchange` closure ([#288](https://github.com/TokamakUI/Tokamak/pull/288)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.4.0 (30 September 2020)
This is mainly a bugfix and compatibility release with a small feature addition. Namely, `Slider`
view is introduced in the DOM renderer, and binding updates for SVG elements are working now. During
this development cycle efforts of our team were devoted to recently released [JavaScriptKit
0.7](https://github.com/swiftwasm/JavaScriptKit/releases/tag/0.7.0) and [`carton`
0.6](https://github.com/swiftwasm/carton/releases/tag/0.6.0). Both of those releases are pretty big
updates that improve developer experience significantly, and this version of Tokamak requires those
as minimum versions.
Many thanks to [@j-f1](https://github.com/j-f1) and
[@kateinoigakukun](https://github.com/kateinoigakukun) for their contributions to these updates!
**Closed issues:**
- HTML + Binding ([#278](https://github.com/TokamakUI/Tokamak/issues/278))
**Merged pull requests:**
- Fix compatibility with JavaScriptKit 0.7 ([#281](https://github.com/TokamakUI/Tokamak/pull/281))
via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Re-export `HTML` type in `TokamakDOM` ([#275](https://github.com/TokamakUI/Tokamak/pull/275)) via
[@kateinoigakukun](https://github.com/kateinoigakukun)
- Use setAttribute, not properties to fix SVG update
([#279](https://github.com/TokamakUI/Tokamak/pull/279)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Allow non-body mount host node ([#271](https://github.com/TokamakUI/Tokamak/pull/271)) via
[@kateinoigakukun](https://github.com/kateinoigakukun)
- Add missing JavaScriptKit import to `README.md`
([#265](https://github.com/TokamakUI/Tokamak/pull/265)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix the sizing of sliders ([#268](https://github.com/TokamakUI/Tokamak/pull/268)) via
[@j-f1](https://github.com/j-f1)
- Add `Slider` implementation ([#228](https://github.com/TokamakUI/Tokamak/pull/228)) via
[@j-f1](https://github.com/j-f1)
- Remove Xcode 12 warning from README.md ([#264](https://github.com/TokamakUI/Tokamak/pull/264)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
# 0.3.0 (19 August 2020)
This release improves compatibility with the SwiftUI API and fixes bugs in our WebAssembly/DOM renderer, included but not limited to:
- support for `App`/`Scene` lifecycle;
- `ColorScheme` detection and environment setting;
- dark mode styles;
- `@StateObject` property wrapper implementation;
- `SidebarListStyle`, `ButtonStyle`, `GeometryProxy` types;
- `NavigationView` and `GeometryReader` views.
Additionally, new `TokamakStaticHTML` renderer was added that supports rendering stateless views into static HTML that doesn't include any JavaScript or WebAssembly dependencies. This is useful for static websites and in the future could be used together with `TokamakDOM` for server-side rendering.
Tokamak 0.3.0 now requires 5.3 snapshots of SwiftWasm, which in general should be more stable than the development snapshots that were previously used, and is also compatible with Xcode 12 betas. If you have a `.swift-version` file in your project, you should specify `wasm-5.3-SNAPSHOT-2020-07-27-a` in it or a later 5.3 snapshot, otherwise `carton` 0.5 selects a compatible 5.3 snapshot for you automatically. Allowing `carton` to select a default snapshot is the recommended approach, so in general we recommend avoiding `.swif-version` files in projects that use Tokamak.
Many thanks to [@carson-katri](https://github.com/carson-katri), [@j-f1](https://github.com/j-f1),
and [@Outcue](https://github.com/Outcue) for their contributions to this release.
The complete list of changes included in this release is available below.
**Closed issues:**
- Command "carton dev" failed ([#258](https://github.com/swiftwasm/Tokamak/issues/258))
- Dark mode detection causes crashes in Safari
([#245](https://github.com/swiftwasm/Tokamak/issues/245))
- Add dark color scheme style ([#237](https://github.com/swiftwasm/Tokamak/issues/237))
- Establish App lifecycle as the only way to start rendering
([#224](https://github.com/swiftwasm/Tokamak/issues/224))
- Runtime issues with dynamic properties in `App` types
([#222](https://github.com/swiftwasm/Tokamak/issues/222))
- `List` appearance changes when reloaded ([#212](https://github.com/swiftwasm/Tokamak/issues/212))
- List scrolling does not work on Firefox 78 on macOS
([#211](https://github.com/swiftwasm/Tokamak/issues/211))
- Scrolling broken when `List` is child of `NavigationView`
([#208](https://github.com/swiftwasm/Tokamak/issues/208))
- `Rectangle` frame is not being set properly
([#185](https://github.com/swiftwasm/Tokamak/issues/185))
- Implement `SidebarListStyle` ([#180](https://github.com/swiftwasm/Tokamak/issues/180))
- Implement `GeometryReader`/`GeometryProxy`
([#176](https://github.com/swiftwasm/Tokamak/issues/176))
- `@StateObject` support ([#158](https://github.com/swiftwasm/Tokamak/issues/158))
- NavigationView/NavigationLink ([#129](https://github.com/swiftwasm/Tokamak/issues/129))
**Merged pull requests:**
- Set versions of dependencies in `Package.swift`
([#262](https://github.com/swiftwasm/Tokamak/pull/262)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Implement `StateObject` property wrapper ([#260](https://github.com/swiftwasm/Tokamak/pull/260))
via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix `NavigationView` broken state after re-render
([#259](https://github.com/swiftwasm/Tokamak/pull/259)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `GeometryReader` implementation ([#239](https://github.com/swiftwasm/Tokamak/pull/239)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add default dark styles for Views ([#241](https://github.com/swiftwasm/Tokamak/pull/241)) via
[@carson-katri](https://github.com/carson-katri)
- Link to the renderers guide from `README.md`
([#251](https://github.com/swiftwasm/Tokamak/pull/251)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Use the latest 5.3 snapshot in `.swift-version`
([#252](https://github.com/swiftwasm/Tokamak/pull/252)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix color scheme observer crashes in Safari
([#249](https://github.com/swiftwasm/Tokamak/pull/249)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Update to the latest version of SwiftFormat
([#250](https://github.com/swiftwasm/Tokamak/pull/250)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Split demo list into sections ([#243](https://github.com/swiftwasm/Tokamak/pull/243)) via
[@j-f1](https://github.com/j-f1)
- Remove some `AnyView` in the `List` implementation
([#246](https://github.com/swiftwasm/Tokamak/pull/246)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `_targetRef` and `_domRef` modifiers ([#240](https://github.com/swiftwasm/Tokamak/pull/240))
via [@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `ColorScheme` environment ([#136](https://github.com/swiftwasm/Tokamak/pull/136)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `redacted` modifier ([#232](https://github.com/swiftwasm/Tokamak/pull/232)) via
[@carson-katri](https://github.com/carson-katri)
- Add Static HTML Renderer and Documentation ([#204](https://github.com/swiftwasm/Tokamak/pull/204))
via [@carson-katri](https://github.com/carson-katri)
- Fix tests, move `DefaultButtonStyle` to TokamakCore
([#234](https://github.com/swiftwasm/Tokamak/pull/234)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Remove `DefaultApp`, make `DOMRenderer` internal
([#227](https://github.com/swiftwasm/Tokamak/pull/227)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add basic `ButtonStyle` implementation ([#214](https://github.com/swiftwasm/Tokamak/pull/214)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Make reconciler tests build and run on macOS
([#229](https://github.com/swiftwasm/Tokamak/pull/229)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix environment changes causing remounted scenes with lost state
([#223](https://github.com/swiftwasm/Tokamak/pull/223)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Add `DefaultApp` type to simplify `DOMRenderer.init`
([#217](https://github.com/swiftwasm/Tokamak/pull/217)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Implement `SidebarListStyle` ([#210](https://github.com/swiftwasm/Tokamak/pull/210)) via
[@Outcue](https://github.com/Outcue)
- Unify code of `MountedApp`/`MountedCompositeView`
([#219](https://github.com/swiftwasm/Tokamak/pull/219)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Generalize style and environment in `DOMRenderer`
([#215](https://github.com/swiftwasm/Tokamak/pull/215)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Implement `DynamicProperty` ([#213](https://github.com/swiftwasm/Tokamak/pull/213)) via
[@carson-katri](https://github.com/carson-katri)
- Warn against beta versions of Xcode in README.md
([#207](https://github.com/swiftwasm/Tokamak/pull/207)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Fix typo in `TokamakDemo.swift` ([#206](https://github.com/swiftwasm/Tokamak/pull/206)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Update "Requirements" and "Getting started" README sections
([#205](https://github.com/swiftwasm/Tokamak/pull/205)) via
[@MaxDesiatov](https://github.com/MaxDesiatov)
- Initial `NavigationView` implementation ([#130](https://github.com/swiftwasm/Tokamak/pull/130))
via [@j-f1](https://github.com/j-f1)
- Add SwiftUI App Lifecycle ([#195](https://github.com/swiftwasm/Tokamak/pull/195)) via
[@carson-katri](https://github.com/carson-katri)
# 0.2.0 (21 July 2020)
This is the first release that supports WebAssembly and browser apps with the new `TokamakDOM`
module. The API now closely follows SwiftUI, while the new React-like API is no longer available.

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
identification within third-party archives.
Copyright 2018-2020 Digital Signal Limited and Tokamak Contributors
Copyright 2018-2021 Digital Signal Limited and Tokamak Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

12
Makefile Executable file
View File

@ -0,0 +1,12 @@
LINKER_FLAGS := $(shell pkg-config --libs gtk+-3.0 gdk-3.0)
C_FLAGS := $(shell pkg-config --cflags gtk+-3.0)
SWIFT_LINKER_FLAGS ?= -Xlinker $(shell echo $(LINKER_FLAGS) | sed -e "s/ / -Xlinker /g" | sed -e "s/-Xlinker -Wl,-framework,/-Xlinker -framework -Xlinker /g")
SWIFT_C_FLAGS ?= -Xcc $(shell echo $(C_FLAGS) | sed -e "s/ / -Xcc /g")
all: build
build:
swift build --enable-test-discovery --product TokamakGTKDemo $(SWIFT_C_FLAGS) $(SWIFT_LINKER_FLAGS)
run: build
.build/debug/TokamakGTKDemo

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -7,45 +7,75 @@
objects = {
/* Begin PBXBuildFile section */
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBD5DE24B3BF090066468A /* ToggleDemo.swift */; };
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */; };
85ED188A24AD3CD60085DFA0 /* macOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188724AD3CC30085DFA0 /* macOS.storyboard */; };
85ED188C24AD3CF10085DFA0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */; };
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */; };
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189B24AD425E0085DFA0 /* TextDemo.swift */; };
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */; };
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */; };
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189E24AD425E0085DFA0 /* Counter.swift */; };
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */; };
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22DF24BC89FD001738DF /* ColorDemo.swift */; };
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56F22E224BD1C26001738DF /* GridDemo.swift */; };
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078726274BD1E5003E787B /* SpacerDemo.swift */; };
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */; };
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078728274BD1E5003E787B /* GridDemo.swift */; };
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078729274BD1E5003E787B /* StackDemo.swift */; };
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872B274BD1E5003E787B /* DatePickerDemo.swift */; };
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872C274BD1E5003E787B /* SliderDemo.swift */; };
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872D274BD1E5003E787B /* PickerDemo.swift */; };
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107872E274BD1E5003E787B /* ToggleDemo.swift */; };
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */; };
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078731274BD1E5003E787B /* ForEachDemo.swift */; };
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
D1078763274BD1E5003E787B /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078732274BD1E5003E787B /* ListDemo.swift */; };
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078733274BD1E5003E787B /* SidebarDemo.swift */; };
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078735274BD1E5003E787B /* TaskDemo.swift */; };
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078736274BD1E5003E787B /* ShadowDemo.swift */; };
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873B274BD1E5003E787B /* PathDemo.swift */; };
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873C274BD1E5003E787B /* CanvasDemo.swift */; };
D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873D274BD1E5003E787B /* ColorDemo.swift */; };
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */; };
D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078740274BD1E5003E787B /* AnimationDemo.swift */; };
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */; };
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078742274BD1E5003E787B /* TransitionDemo.swift */; };
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078743274BD1E5003E787B /* ProgressViewDemo.swift */; };
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078744274BD1E5003E787B /* AppStorageDemo.swift */; };
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078745274BD1E5003E787B /* EnvironmentDemo.swift */; };
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078746274BD1E5003E787B /* RedactDemo.swift */; };
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078748274BD1E5003E787B /* TextDemo.swift */; };
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1078749274BD1E5003E787B /* TextEditorDemo.swift */; };
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874A274BD1E5003E787B /* TextFieldDemo.swift */; };
D107878A274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
D107878B274BD1E5003E787B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874C274BD1E5003E787B /* Counter.swift */; };
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */; };
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -78,32 +108,48 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TokamakDemo Native.entitlements"; sourceTree = "<group>"; };
85ED184A24AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
85ED185224AD379A0085DFA0 /* TokamakDemo Native.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TokamakDemo Native.app"; sourceTree = BUILT_PRODUCTS_DIR; };
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAppDelegate.swift; sourceTree = "<group>"; };
85ED188724AD3CC30085DFA0 /* macOS.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = macOS.storyboard; sourceTree = "<group>"; };
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAppDelegate.swift; sourceTree = "<group>"; };
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
85ED189B24AD425E0085DFA0 /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; };
85ED189E24AD425E0085DFA0 /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
B56F22DF24BC89FD001738DF /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
B56F22E224BD1C26001738DF /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1078726274BD1E5003E787B /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = "<group>"; };
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
D1078728274BD1E5003E787B /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = "<group>"; };
D1078729274BD1E5003E787B /* StackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackDemo.swift; sourceTree = "<group>"; };
D107872B274BD1E5003E787B /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
D107872C274BD1E5003E787B /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
D107872D274BD1E5003E787B /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
D107872E274BD1E5003E787B /* ToggleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleDemo.swift; sourceTree = "<group>"; };
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1078731274BD1E5003E787B /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = "<group>"; };
D1078732274BD1E5003E787B /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1078733274BD1E5003E787B /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
D1078735274BD1E5003E787B /* TaskDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskDemo.swift; sourceTree = "<group>"; };
D1078736274BD1E5003E787B /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
D107873B274BD1E5003E787B /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
D107873C274BD1E5003E787B /* CanvasDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasDemo.swift; sourceTree = "<group>"; };
D107873D274BD1E5003E787B /* ColorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorDemo.swift; sourceTree = "<group>"; };
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
D1078740274BD1E5003E787B /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
D1078742274BD1E5003E787B /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
D1078744274BD1E5003E787B /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
D1078746274BD1E5003E787B /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
D1078748274BD1E5003E787B /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = "<group>"; };
D1078749274BD1E5003E787B /* TextEditorDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEditorDemo.swift; sourceTree = "<group>"; };
D107874A274BD1E5003E787B /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = "<group>"; };
D107874C274BD1E5003E787B /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -136,6 +182,7 @@
85ED183D24AD37970085DFA0 = {
isa = PBXGroup;
children = (
8587DF5524D4B9A40033EF43 /* TokamakDemo Native.entitlements */,
D1E5FDAB24C1D57000E7485E /* TokamakShim */,
85ED188B24AD3CF10085DFA0 /* LaunchScreen.storyboard */,
85ED186924AD38F20085DFA0 /* UIAppDelegate.swift */,
@ -162,27 +209,106 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
85ED189E24AD425E0085DFA0 /* Counter.swift */,
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */,
85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */,
B56F22E224BD1C26001738DF /* GridDemo.swift */,
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */,
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
B51F214F24B920B400CF2583 /* PathDemo.swift */,
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */,
85ED189B24AD425E0085DFA0 /* TextDemo.swift */,
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
85CBD5DE24B3BF090066468A /* ToggleDemo.swift */,
D107874B274BD1E5003E787B /* Buttons */,
D107872F274BD1E5003E787B /* Containers */,
D107873A274BD1E5003E787B /* Drawing */,
D1078725274BD1E5003E787B /* Layout */,
D107873F274BD1E5003E787B /* Misc */,
D1078734274BD1E5003E787B /* Modifiers */,
D107872A274BD1E5003E787B /* Selectors */,
D1078747274BD1E5003E787B /* Text */,
85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */,
);
name = TokamakDemo;
path = ../Sources/TokamakDemo;
sourceTree = "<group>";
};
D1078725274BD1E5003E787B /* Layout */ = {
isa = PBXGroup;
children = (
D1078726274BD1E5003E787B /* SpacerDemo.swift */,
D1078727274BD1E5003E787B /* GeometryReaderDemo.swift */,
D1078728274BD1E5003E787B /* GridDemo.swift */,
D1078729274BD1E5003E787B /* StackDemo.swift */,
);
path = Layout;
sourceTree = "<group>";
};
D107872A274BD1E5003E787B /* Selectors */ = {
isa = PBXGroup;
children = (
D107872B274BD1E5003E787B /* DatePickerDemo.swift */,
D107872C274BD1E5003E787B /* SliderDemo.swift */,
D107872D274BD1E5003E787B /* PickerDemo.swift */,
D107872E274BD1E5003E787B /* ToggleDemo.swift */,
);
path = Selectors;
sourceTree = "<group>";
};
D107872F274BD1E5003E787B /* Containers */ = {
isa = PBXGroup;
children = (
D1078730274BD1E5003E787B /* OutlineGroupDemo.swift */,
D1078731274BD1E5003E787B /* ForEachDemo.swift */,
D1078732274BD1E5003E787B /* ListDemo.swift */,
D1078733274BD1E5003E787B /* SidebarDemo.swift */,
);
path = Containers;
sourceTree = "<group>";
};
D1078734274BD1E5003E787B /* Modifiers */ = {
isa = PBXGroup;
children = (
D1078735274BD1E5003E787B /* TaskDemo.swift */,
D1078736274BD1E5003E787B /* ShadowDemo.swift */,
);
path = Modifiers;
sourceTree = "<group>";
};
D107873A274BD1E5003E787B /* Drawing */ = {
isa = PBXGroup;
children = (
D107873B274BD1E5003E787B /* PathDemo.swift */,
D107873C274BD1E5003E787B /* CanvasDemo.swift */,
D107873D274BD1E5003E787B /* ColorDemo.swift */,
D107873E274BD1E5003E787B /* ShapeStyleDemo.swift */,
);
path = Drawing;
sourceTree = "<group>";
};
D107873F274BD1E5003E787B /* Misc */ = {
isa = PBXGroup;
children = (
D1078740274BD1E5003E787B /* AnimationDemo.swift */,
D1078741274BD1E5003E787B /* PreferenceKeyDemo.swift */,
D1078742274BD1E5003E787B /* TransitionDemo.swift */,
D1078743274BD1E5003E787B /* ProgressViewDemo.swift */,
D1078744274BD1E5003E787B /* AppStorageDemo.swift */,
D1078745274BD1E5003E787B /* EnvironmentDemo.swift */,
D1078746274BD1E5003E787B /* RedactDemo.swift */,
);
path = Misc;
sourceTree = "<group>";
};
D1078747274BD1E5003E787B /* Text */ = {
isa = PBXGroup;
children = (
D1078748274BD1E5003E787B /* TextDemo.swift */,
D1078749274BD1E5003E787B /* TextEditorDemo.swift */,
D107874A274BD1E5003E787B /* TextFieldDemo.swift */,
);
path = Text;
sourceTree = "<group>";
};
D107874B274BD1E5003E787B /* Buttons */ = {
isa = PBXGroup;
children = (
D107874C274BD1E5003E787B /* Counter.swift */,
D107874D274BD1E5003E787B /* ButtonStyleDemo.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
D1E5FDAB24C1D57000E7485E /* TokamakShim */ = {
isa = PBXGroup;
children = (
@ -321,23 +447,38 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D107875C274BD1E5003E787B /* ToggleDemo.swift in Sources */,
D1078784274BD1E5003E787B /* TextDemo.swift in Sources */,
D107878A274BD1E5003E787B /* Counter.swift in Sources */,
D1078752274BD1E5003E787B /* GridDemo.swift in Sources */,
D1078774274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D107875E274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
D1078770274BD1E5003E787B /* CanvasDemo.swift in Sources */,
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */,
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */,
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
D1078758274BD1E5003E787B /* SliderDemo.swift in Sources */,
D107877A274BD1E5003E787B /* TransitionDemo.swift in Sources */,
D1078768274BD1E5003E787B /* ShadowDemo.swift in Sources */,
D107877E274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
D1078782274BD1E5003E787B /* RedactDemo.swift in Sources */,
D1078786274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
D1078772274BD1E5003E787B /* ColorDemo.swift in Sources */,
D1078776274BD1E5003E787B /* AnimationDemo.swift in Sources */,
D1078756274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
D1078780274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
D107876E274BD1E5003E787B /* PathDemo.swift in Sources */,
D1078764274BD1E5003E787B /* SidebarDemo.swift in Sources */,
D1078754274BD1E5003E787B /* StackDemo.swift in Sources */,
D1078778274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
D107874E274BD1E5003E787B /* SpacerDemo.swift in Sources */,
D1078766274BD1E5003E787B /* TaskDemo.swift in Sources */,
D1078760274BD1E5003E787B /* ForEachDemo.swift in Sources */,
D1078750274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
D107875A274BD1E5003E787B /* PickerDemo.swift in Sources */,
D1078788274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
D107878C274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
D1078762274BD1E5003E787B /* ListDemo.swift in Sources */,
D107877C274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -345,23 +486,38 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D107875D274BD1E5003E787B /* ToggleDemo.swift in Sources */,
D1078785274BD1E5003E787B /* TextDemo.swift in Sources */,
D107878B274BD1E5003E787B /* Counter.swift in Sources */,
D1078753274BD1E5003E787B /* GridDemo.swift in Sources */,
D1078775274BD1E5003E787B /* ShapeStyleDemo.swift in Sources */,
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
D107875F274BD1E5003E787B /* OutlineGroupDemo.swift in Sources */,
D1078771274BD1E5003E787B /* CanvasDemo.swift in Sources */,
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */,
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
D1078759274BD1E5003E787B /* SliderDemo.swift in Sources */,
D107877B274BD1E5003E787B /* TransitionDemo.swift in Sources */,
D1078769274BD1E5003E787B /* ShadowDemo.swift in Sources */,
D107877F274BD1E5003E787B /* AppStorageDemo.swift in Sources */,
D1078783274BD1E5003E787B /* RedactDemo.swift in Sources */,
D1078787274BD1E5003E787B /* TextEditorDemo.swift in Sources */,
D1078773274BD1E5003E787B /* ColorDemo.swift in Sources */,
D1078777274BD1E5003E787B /* AnimationDemo.swift in Sources */,
D1078757274BD1E5003E787B /* DatePickerDemo.swift in Sources */,
D1078781274BD1E5003E787B /* EnvironmentDemo.swift in Sources */,
D107876F274BD1E5003E787B /* PathDemo.swift in Sources */,
D1078765274BD1E5003E787B /* SidebarDemo.swift in Sources */,
D1078755274BD1E5003E787B /* StackDemo.swift in Sources */,
D1078779274BD1E5003E787B /* PreferenceKeyDemo.swift in Sources */,
D107874F274BD1E5003E787B /* SpacerDemo.swift in Sources */,
D1078767274BD1E5003E787B /* TaskDemo.swift in Sources */,
D1078761274BD1E5003E787B /* ForEachDemo.swift in Sources */,
D1078751274BD1E5003E787B /* GeometryReaderDemo.swift in Sources */,
D107875B274BD1E5003E787B /* PickerDemo.swift in Sources */,
D1078789274BD1E5003E787B /* TextFieldDemo.swift in Sources */,
D107878D274BD1E5003E787B /* ButtonStyleDemo.swift in Sources */,
D1078763274BD1E5003E787B /* ListDemo.swift in Sources */,
D107877D274BD1E5003E787B /* ProgressViewDemo.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -509,17 +665,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 288H3WAR3W;
CODE_SIGN_ENTITLEMENTS = "TokamakDemo Native.entitlements";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "iOS Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -531,17 +692,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 288H3WAR3W;
CODE_SIGN_ENTITLEMENTS = "TokamakDemo Native.entitlements";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "iOS Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
@ -554,7 +720,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "";
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
@ -564,7 +730,10 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -578,9 +747,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "";
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = 288H3WAR3W;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Info.plist;
@ -588,7 +757,10 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.tokamak.Tokamak-Native";
PRODUCT_NAME = "TokamakDemo Native";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_VERSION = 5.0;

View File

@ -1,34 +1,59 @@
{
"object": {
"pins": [
{
"package": "JavaScriptKit",
"repositoryURL": "https://github.com/kateinoigakukun/JavaScriptKit.git",
"state": {
"branch": "c90e82f",
"revision": "c90e82fe1d576a2ccd1aae798380bf80be7885fb",
"version": null
}
},
{
"package": "OpenCombine",
"repositoryURL": "https://github.com/MaxDesiatov/OpenCombine.git",
"state": {
"branch": "observable-object",
"revision": "3c3a181acad7ab44a64d7c41140eb843222bb2aa",
"version": null
}
},
{
"package": "Runtime",
"repositoryURL": "https://github.com/MaxDesiatov/Runtime.git",
"state": {
"branch": "wasi-build",
"revision": "a9309b4822d6dd0e4a8e92351ee9e3d210e19b4e",
"version": null
}
"pins" : [
{
"identity" : "javascriptkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftwasm/JavaScriptKit.git",
"state" : {
"revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
"version" : "0.15.0"
}
]
},
"version": 1
},
{
"identity" : "opencombine",
"kind" : "remoteSourceControl",
"location" : "https://github.com/OpenCombine/OpenCombine.git",
"state" : {
"revision" : "9cf67e363738dbab61b47fb5eaed78d3db31e5ee",
"version" : "0.13.0"
}
},
{
"identity" : "opencombinejs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftwasm/OpenCombineJS.git",
"state" : {
"revision" : "e574e418ba468ff5c2d4c499eb56f108aeb4d2ba",
"version" : "0.2.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
"version" : "1.1.2"
}
},
{
"identity" : "swift-benchmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/swift-benchmark",
"state" : {
"revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
"version" : "0.1.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "f8a9c997c3c1dab4e216a8ec9014e23144cbab37",
"version" : "1.9.0"
}
}
],
"version" : 2
}

View File

@ -1,13 +1,11 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to
// build this package.
// swift-tools-version:5.6
import PackageDescription
let package = Package(
name: "Tokamak",
platforms: [
.macOS(.v10_15),
.macOS(.v11),
.iOS(.v13),
],
products: [
@ -21,54 +19,216 @@ let package = Package(
name: "TokamakDOM",
targets: ["TokamakDOM"]
),
.library(
name: "TokamakStaticHTML",
targets: ["TokamakStaticHTML"]
),
.executable(
name: "TokamakStaticHTMLDemo",
targets: ["TokamakStaticHTMLDemo"]
),
.library(
name: "TokamakGTK",
targets: ["TokamakGTK"]
),
.executable(
name: "TokamakGTKDemo",
targets: ["TokamakGTKDemo"]
),
.library(
name: "TokamakShim",
targets: ["TokamakShim"]
),
.executable(
name: "TokamakStaticHTMLBenchmark",
targets: ["TokamakStaticHTMLBenchmark"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/kateinoigakukun/JavaScriptKit.git", .revision("c90e82f")),
.package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build")),
.package(url: "https://github.com/MaxDesiatov/OpenCombine.git", .branch("observable-object")),
.package(
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
from: "0.15.0"
),
.package(
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
from: "0.12.0"
),
.package(
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
from: "0.2.0"
),
.package(
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
from: "0.1.2"
),
.package(
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
from: "1.9.0"
),
],
targets: [
// Targets are the basic building blocks of a package. A target can define
// a module or a test suite.
// Targets can depend on other targets in this package, and on products
// in packages which this package depends on.
.target(
name: "CombineShim",
dependencies: [.product(
name: "OpenCombine",
package: "OpenCombine",
condition: .when(platforms: [.wasi, .linux])
)]
),
.target(
name: "TokamakCore",
dependencies: ["CombineShim", "Runtime"]
),
.target(
name: "TokamakDemo",
dependencies: ["JavaScriptKit", "TokamakShim"]
),
.target(
name: "TokamakDOM",
dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore"]
dependencies: [
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
]
),
.target(
name: "TokamakShim",
dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))]
dependencies: [
.target(name: "TokamakDOM", condition: .when(platforms: [.wasi])),
.target(name: "TokamakGTK", condition: .when(platforms: [.linux])),
]
),
.systemLibrary(
name: "CGTK",
pkgConfig: "gtk+-3.0",
providers: [
.apt(["libgtk+-3.0", "gtk+-3.0"]),
// .yum(["gtk3-devel"]),
.brew(["gtk+3"]),
]
),
.systemLibrary(
name: "CGDK",
pkgConfig: "gdk-3.0",
providers: [
.apt(["libgtk+-3.0", "gtk+-3.0"]),
// .yum(["gtk3-devel"]),
.brew(["gtk+3"]),
]
),
.target(
name: "TokamakGTKCHelpers",
dependencies: ["CGTK"]
),
.target(
name: "TokamakGTK",
dependencies: [
"TokamakCore", "CGTK", "CGDK", "TokamakGTKCHelpers",
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
]
),
.executableTarget(
name: "TokamakGTKDemo",
dependencies: ["TokamakGTK"],
resources: [.copy("logo-header.png")]
),
.target(
name: "TokamakStaticHTML",
dependencies: [
"TokamakCore",
]
),
.executableTarget(
name: "TokamakCoreBenchmark",
dependencies: [
.product(name: "Benchmark", package: "swift-benchmark"),
"TokamakCore",
"TokamakTestRenderer",
]
),
.executableTarget(
name: "TokamakStaticHTMLBenchmark",
dependencies: [
.product(name: "Benchmark", package: "swift-benchmark"),
"TokamakStaticHTML",
]
),
.target(
name: "TokamakDOM",
dependencies: [
"TokamakCore",
"TokamakStaticHTML",
.product(
name: "OpenCombineShim",
package: "OpenCombine"
),
.product(
name: "JavaScriptKit",
package: "JavaScriptKit",
condition: .when(platforms: [.wasi])
),
.product(
name: "JavaScriptEventLoop",
package: "JavaScriptKit",
condition: .when(platforms: [.wasi])
),
"OpenCombineJS",
]
),
.executableTarget(
name: "TokamakDemo",
dependencies: [
"TokamakShim",
.product(
name: "JavaScriptKit",
package: "JavaScriptKit",
condition: .when(platforms: [.wasi])
),
],
resources: [.copy("logo-header.png")],
linkerSettings: [
.unsafeFlags(
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
.when(platforms: [.wasi])
),
]
),
.executableTarget(
name: "TokamakStaticHTMLDemo",
dependencies: [
"TokamakStaticHTML",
]
),
.target(
name: "TokamakTestRenderer",
dependencies: ["TokamakCore"]
),
.testTarget(
name: "TokamakLayoutTests",
dependencies: [
"TokamakCore",
"TokamakStaticHTML",
.product(
name: "SnapshotTesting",
package: "swift-snapshot-testing",
condition: .when(platforms: [.macOS])
),
]
),
.testTarget(
name: "TokamakReconcilerTests",
dependencies: [
"TokamakCore",
"TokamakTestRenderer",
]
),
.testTarget(
name: "TokamakTests",
dependencies: ["TokamakDemo", "TokamakTestRenderer"]
dependencies: ["TokamakTestRenderer"]
),
.testTarget(
name: "TokamakStaticHTMLTests",
dependencies: [
"TokamakStaticHTML",
.product(
name: "SnapshotTesting",
package: "swift-snapshot-testing",
condition: .when(platforms: [.macOS])
),
],
exclude: ["__Snapshots__", "RenderingTests/__Snapshots__"]
),
]
)

316
README.md
View File

@ -1,25 +1,28 @@
<img alt="Tokamak logo" src="docs/logo-header.png" width="640px"/>
<img alt="Tokamak logo" src="Sources/TokamakDemo/logo-header.png" width="640px"/>
## SwiftUI-compatible framework for building browser apps with WebAssembly
![CI status](https://github.com/swiftwasm/Tokamak/workflows/CI/badge.svg?branch=main)
[![CI status](https://github.com/swiftwasm/Tokamak/workflows/CI/badge.svg?branch=main)](https://github.com/TokamakUI/Tokamak/actions?query=workflow%3ACI) [![Discord](https://img.shields.io/discord/780838335798706197?label=Discord)](https://discord.gg/ashJW8T8yp)
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports
a few view types and modifiers (you can check the current list in [the progress document](docs/progress.md)),
and a new `HTML` view for constructing arbitrary HTML. The long-term goal of Tokamak is to implement
as much of SwiftUI API as possible and to provide a few more helpful additions that simplify HTML
and CSS interactions.
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports a few
view types and modifiers (you can check the current list in [the progress
document](docs/progress.md)), and a new `HTML` view for constructing arbitrary HTML. The long-term
goal of Tokamak is to implement as much of SwiftUI API as possible and to provide a few more helpful
additions that simplify HTML and CSS interactions.
If there's some SwiftUI API that's missing but you'd like to use it, please review the existing
[issues](https://github.com/swiftwasm/Tokamak/issues) and [PRs](https://github.com/swiftwasm/Tokamak/pulls)
to get more details about the current status, or [create a new issue](https://github.com/swiftwasm/Tokamak/issues/new)
to let us prioritize the development based on the demand. We also try to make the development of
views and modifiers easier (with the help from the `HTML` view, see [the example
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome! Don't
forget to check [the "Contributing" section](https://github.com/swiftwasm/Tokamak#contributing) first.
[issues](https://github.com/swiftwasm/Tokamak/issues) and
[PRs](https://github.com/swiftwasm/Tokamak/pulls) to get more details about the current status, or
[create a new issue](https://github.com/swiftwasm/Tokamak/issues/new) to let us prioritize the
development based on the demand. We also try to make the development of views and modifiers easier
(with the help from the `HTML` view, see [the example
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome!
Don't forget to check [the "Contributing"
section](https://github.com/swiftwasm/Tokamak#contributing) first.
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're also very
welcome to join the `#webassembly` channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/).
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're
also very welcome to join [our Discord server](https://discord.gg/ashJW8T8yp), or the `#webassembly`
channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/).
### Example code
@ -48,23 +51,15 @@ struct Counter: View {
}
}
}
```
You can then render your view in any DOM node captured with
[JavaScriptKit](https://github.com/kateinoigakukun/JavaScriptKit/), just
pass it as an argument to the `DOMRenderer` initializer together with your view:
```swift
import JavaScriptKit
import TokamakDOM
let document = JSObjectRef.global.document.object!
let divElement = document.createElement!("div").object!
let renderer = DOMRenderer(Counter(count: 5, limit: 15), divElement)
let body = document.body.object!
_ = body.appendChild!(divElement)
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
```
### Arbitrary HTML
@ -84,17 +79,55 @@ struct SVGCircle: View {
}
```
`HTML` doesn't support event listeners, and is declared in the `TokamakStaticHTML` module, which `TokamakDOM` re-exports. The benefit of `HTML` is that you can use it for static rendering in libraries like [TokamakVapor](https://github.com/TokamakUI/TokamakVapor) and [TokamakPublish](https://github.com/TokamakUI/TokamakPublish).
Another option is the `DynamicHTML` view provided by the `TokamakDOM` module, which has a `listeners` property with a corresponding initializer parameter. You can pass closures that can handle `onclick`, `onmouseover` and other DOM events for you in the `listeners` dictionary. Check out [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers) for the full list.
An example of mouse events handling with `DynamicHTML` would look like this:
```swift
struct MouseEventsView: View {
@State var position: CGPoint = .zero
@State var isMouseButtonDown: Bool = false
var body: some View {
DynamicHTML(
"div",
["style": "width: 200px; height: 200px; background-color: red;"],
listeners: [
"mousemove": { event in
guard
let x = event.offsetX.jsValue.number,
let y = event.offsetY.jsValue.number
else { return }
position = CGPoint(x: x, y: y)
},
"mousedown": { _ in isMouseButtonDown = true },
"mouseup": { _ in isMouseButtonDown = false },
]
) {
Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
}
}
}
```
### Arbitrary styles and scripts
While `JavaScriptKit` is a great option for occasional interactions with JavaScript,
While [`JavaScriptKit`](https://github.com/swiftwasm/JavaScriptKit) is a great option for occasional interactions with JavaScript,
sometimes you need to inject arbitrary scripts or styles, which can be done through direct
DOM access:
```swift
_ = document.head.object!.insertAdjacentHTML!("beforeend", #"""
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js"></script>
"""#)
_ = document.head.object!.insertAdjacentHTML!("beforeend", #"""
import JavaScriptKit
let document = JSObject.global.document
let script = document.createElement("script")
script.setAttribute("src", "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js")
document.head.appendChild(script)
_ = document.head.insertAdjacentHTML("beforeend", #"""
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
@ -105,30 +138,69 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
localized date formatting (or any arbitrary style/script/font added that way) are available in your
app.
## Requirements for app developers
### Fiber renderers
- macOS 10.15 and Xcode 11.4/11.5/11.6 for macOS. Xcode betas are currently not supported. You can have
those installed, but please make sure you use
[`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_)
to point it to a release version of Xcode.
- [Swift 5.2 or later](https://swift.org/download/) for Linux.
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
is optionally available. It can provide faster updates and allow for larger View hierarchies.
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
## Requirements for app users
You can specify which reconciler to use in your `App`'s configuration:
Any browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) should work, which currently includes:
```swift
struct CounterApp: App {
static let _configuration: _AppConfiguration = .init(
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
reconciler: .fiber(useDynamicLayout: true)
)
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
```
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
## Requirements
### For app developers
- macOS 11 and Xcode 13.2 or later when using VS Code. macOS 12 and Xcode 13.3 or later are recommended if
you'd like to use Xcode for auto-completion, or when developing multi-platform apps that target WebAssembly
and macOS at the same time.
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
Other Linux distributions are currently not supported.
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
### For users of apps depending on Tokamak
Any recent browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) and [required
JavaScript features](https://caniuse.com/?search=finalizationregistry) should work, which currently includes:
- Edge 84+
- Firefox 79+
- Chrome 84+
- Desktop Safari 14.1+
- Mobile Safari 14.8+
If you need to support older browser versions, you'll have to build with
`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags
when compiling. This should lower browser requirements to these versions:
- Edge 16+
- Firefox 53+
- Chrome 57+
- (Mobile) Safari 11+
- Firefox 61+
- Chrome 66+
- (Mobile) Safari 12+
Not all of these were tested though, compatibility reports are very welcome!
Not all of these versions are tested on regular basis though, compatibility reports are very welcome!
## Getting started
Tokamak relies on [`carton`](https://carton.dev) as a primary build tool. As a part of these steps
you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (unfortunately you'll have to build
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
app by following these steps:
1. Install `carton`:
@ -137,7 +209,7 @@ app by following these steps:
brew install swiftwasm/tap/carton
```
If you had `carton` installed before this, make sure you have version 0.4.1 or greater:
If you had `carton` installed before this, make sure you have version 0.15.0 or greater:
```
carton --version
@ -156,79 +228,82 @@ carton init --template tokamak
```
4. Build the project and start the development server, `carton dev` can be kept running
during development:
during development:
```
carton dev
```
5. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser to see the app
running. You can edit the app source code in your favorite editor and save it, `carton`
will immediately rebuild the app and reload all browser tabs that have the app open.
running. You can edit the app source code in your favorite editor and save it, `carton`
will immediately rebuild the app and reload all browser tabs that have the app open.
You can also clone this repository and run `carton dev` in its root directory. This
will build the demo app that shows almost all of the currently implemented APIs.
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
directory. This will build the demo app that shows almost all of the currently implemented APIs.
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
## Security
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
to override this functionality, you can use the `_domTextSanitizer` modifier:
```swift
Text("<font color='red'>Unsanitized Text</font>")
._domTextSanitizer(Sanitizers.HTML.insecure)
```
You can also use custom sanitizers; the argument to `_domTextSanitizer` is simply a
`String -> String` closure. If `_domTextSanitizer` is applied to a non-`Text` view,
it will apply to all `Text` in subviews, unless overridden.
If you use user-generated or otherwise unsafe strings elsewhere, make sure to properly
sanitize them yourself.
## Troubleshooting
### `unable to find utility "xctest"` error when building
This error can only happen on macOS, so make sure you have Xcode installed as listed [in the
requirements](#requirements-for-app-developers). If you do have Xcode installed but still get the
error, please refer to [this StackOverflow answer](https://stackoverflow.com/a/61725799/442427).
### Syntax highlighting and autocomplete don't work in Xcode
Open `Package.swift` of your project that depends on Tokamak with Xcode and build it for macOS.
As Xcode currently doesn't support cross-compilation for non-Apple platforms, your project can't
be indexed if it doesn't build for macOS, even if it isn't fully function on macOS when running.
If you need to exclude some WebAssembly-specific code in your own app that doesn't compile on macOS,
you can rely on `#if os(WASI)` compiler directives.
All relevant modules of Tokamak (including `TokamakDOM`) should compile on macOS. You may see issues
with `TokamakShim` on macOS Catalina, where relevant SwiftUI APIs aren't supported, but replacing
`import TokamakShim` with `import TokamakDOM` should resolve the issue until you're able to update
to macOS Big Sur.
If you stumble upon code in Tokamak that doesn't build on macOS and prevents syntax highlighting or
autocomplete from working in Xcode, please [report it as a
bug](https://github.com/TokamakUI/Tokamak/issues/new).
### Syntax highlighting and autocomplete don't work in VSCode
Make sure you have [the SourceKit LSP
extension](https://marketplace.visualstudio.com/items?itemName=pvasek.sourcekit-lsp--dev-unofficial)
installed. If you don't trust this unofficial release, please follow [the manual building and
installation guide](https://github.com/apple/sourcekit-lsp/tree/main/Editors/vscode). Apple currently
doesn't provide an official build of the extension on the VSCode Marketplace unfortunately.
## Contributing
### Modular structure
All contributions, no matter how small, are very welcome. You don't have to be a web developer or a
SwiftUI expert to meaningfully contribute. In fact, by checking out how some of the simplest views are
implemented in Tokamak you may learn more how SwiftUI may work under the hood.
Tokamak is built with modularity in mind, providing a cross-platform `TokamakCore` module and
separate modules for platform-specific renderers. Currently, the only available renderer module
is `TokamakDOM`, but we intend to provide other renderers in the future, such as `TokamakHTML`
for static websites and server-side rendering. Tokamak users only need to import a renderer module
they would like to use, while `TokamakCore` is hidden as an "internal" `Tokamak` package target.
Unfortunately, Swift does not allow us to specify that certain symbols in `TokamakCore` are private
to a package, but they need to stay `public` for renderer modules to get access to them. Thus, the
current workaround is to mark those symbols with underscores in their names to indicate this. It
can be formulated as these "rules":
1. If a symbol is restricted to a module and has no `public` access control, no need for an underscore.
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an underscore,
users may use those symbols directly, and it is re-exported from `TokamakCore` by the renderer module
via `public typealias`.
3. If a function or a type have `public` on them only by necessity to make them available in `TokamakDOM`,
but unavailable to users (or not intended for public use), underscore is needed to indicate that.
The benefit of separate modules is that they allow us to provide separate renderers for different platforms.
Users can pick and choose what they want to use, e.g. purely static websites would use only `TokamakHTML`,
single-page apps would use `TokamakDOM`, maybe in conjuction with `TokamakHTML` for pre-rendering. As we'd
like to try to implement a native renderer for Android at some point, probably in a separate `TokamakAndroid`
module, Android apps would use `TokamakAndroid` with no need to be aware of any of the web modules.
### Sponsorship
If this library saved you any amount of time or money, please consider [sponsoring
the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the
sponsorship tiers give you priority support or even consulting time, any amount is
appreciated and helps in maintaining the project.
### Coding Style
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
and [SwiftLint](https://github.com/realm/SwiftLint) to
enforce formatting and coding style. We encourage you to run SwiftFormat within
a local clone of the repository in whatever way works best for you either
manually or automatically via an [Xcode
extension](https://github.com/nicklockwood/SwiftFormat#xcode-source-editor-extension),
[build phase](https://github.com/nicklockwood/SwiftFormat#xcode-build-phase) or
[git pre-commit
hook](https://github.com/nicklockwood/SwiftFormat#git-pre-commit-hook) etc.
To guarantee that these tools run before you commit your changes on macOS, you're encouraged
to run this once to set up the [pre-commit](https://pre-commit.com/) hook:
```
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit
```
Refer to [the pre-commit documentation page](https://pre-commit.com/) for more details
and installation instructions for other platforms.
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can
fail with inconsistent formatting or style. We require CI builds to pass for all
PRs before merging.
Updating our [documentation](https://github.com/TokamakUI/Tokamak/tree/main/docs) and taking on [the starter
bugs](https://github.com/TokamakUI/Tokamak/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
is also appreciated. Don't forget to join our [Discord server](https://discord.gg/ashJW8T8yp) to get in
touch with the maintainers and other users. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more details.
### Code of Conduct
@ -237,10 +312,23 @@ Conduct](https://github.com/swiftwasm/Tokamak/blob/main/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report
unacceptable behavior to conduct@tokamak.dev.
### Sponsorship
If this library saved you any amount of time or money, please consider sponsoring
the work of its maintainers on their sponsorship pages:
[@carson-katri](https://github.com/sponsors/carson-katri),
[@kateinoigakukun](https://github.com/sponsors/kateinoigakukun), and
[@MaxDesiatov](https://github.com/sponsors/MaxDesiatov). While some of the
sponsorship tiers give you priority support or even consulting time, any amount is
appreciated and helps in maintaining the project.
## Maintainers
[Carson Katri](https://github.com/carson-katri),
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com).
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
[Ezra Berch](https://github.com/ezraberch),
[Jed Fox](https://jedfox.com),
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
[Yuta Saito](https://github.com/kateinoigakukun/).
## Acknowledgments

View File

@ -0,0 +1 @@
#include <gdk/gdk.h>

View File

@ -0,0 +1,8 @@
module CGDK {
header "./termios-Header.h"
header "./CGDK-Bridging-Header.h"
link "gdk-3"
export *
}

View File

@ -0,0 +1 @@
#include <termios.h>

View File

@ -0,0 +1 @@
#include <gtk/gtk.h>

View File

@ -0,0 +1,8 @@
module CGTK {
header "./termios-Header.h"
header "./CGTK-Bridging-Header.h"
link "gtk-3"
export *
}

View File

@ -0,0 +1 @@
#include <termios.h>

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.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/11/21.
//
#if canImport(Combine)
@_exported import Combine
#else
@_exported import OpenCombine
#endif
public protocol AnimatableModifier: Animatable, ViewModifier {}

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

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,8 +15,7 @@
// Created by Carson Katri on 7/16/20.
//
import CombineShim
import Runtime
import OpenCombineShim
/// Provides the ability to set the title of the Scene.
public protocol _TitledApp {
@ -29,19 +28,49 @@ public protocol App: _TitledApp {
var body: Body { get }
/// Implemented by the renderer to mount the `App`
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
static func _launch(
_ app: Self,
with configuration: _AppConfiguration
)
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
var _phasePublisher: CurrentValueSubject<ScenePhase, Never> { get }
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
static var _configuration: _AppConfiguration { get }
static func main()
init()
}
extension App {
public static func main() {
let app = Self()
_launch(app, EnvironmentValues())
public struct _AppConfiguration {
public let reconciler: Reconciler
public let rootEnvironment: EnvironmentValues
public init(
reconciler: Reconciler = .stack,
rootEnvironment: EnvironmentValues = .init()
) {
self.reconciler = reconciler
self.rootEnvironment = rootEnvironment
}
public enum Reconciler {
/// Use the `StackReconciler`.
case stack
/// Use the `FiberReconciler` with layout steps optionally enabled.
case fiber(useDynamicLayout: Bool = false)
}
}
public extension App {
static var _configuration: _AppConfiguration { .init() }
static func main() {
let app = Self()
_launch(app, with: Self._configuration)
}
}

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");
// you may not use this file except in compliance with the License.
@ -15,11 +15,15 @@
// Created by Carson Katri on 7/16/20.
//
import CombineShim
import OpenCombineShim
@propertyWrapper public struct AppStorage<Value>: DynamicProperty {
@propertyWrapper
public struct AppStorage<Value>: DynamicProperty {
let provider: _StorageProvider?
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
@Environment(\._defaultAppStorage)
var defaultProvider: _StorageProvider?
var unwrappedProvider: _StorageProvider {
provider ?? defaultProvider!
}
@ -53,10 +57,10 @@ import CombineShim
extension AppStorage: ObservedProperty {}
extension AppStorage {
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Bool {
public extension AppStorage {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Bool
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -64,9 +68,9 @@ extension AppStorage {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Int {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Int
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -74,9 +78,9 @@ extension AppStorage {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Double {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Double
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -84,9 +88,9 @@ extension AppStorage {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == String {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == String
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -94,10 +98,9 @@ extension AppStorage {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil)
where Value: RawRepresentable, Value.RawValue == Int {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value: RawRepresentable, Value.RawValue == Int
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -110,10 +113,9 @@ extension AppStorage {
}
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil)
where Value: RawRepresentable, Value.RawValue == String {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value: RawRepresentable, Value.RawValue == String
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -127,10 +129,10 @@ extension AppStorage {
}
}
extension AppStorage where Value: ExpressibleByNilLiteral {
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Bool? {
public extension AppStorage where Value: ExpressibleByNilLiteral {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Bool?
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -138,9 +140,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Int? {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Int?
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -148,9 +150,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == Double? {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == Double?
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -158,9 +160,9 @@ extension AppStorage where Value: ExpressibleByNilLiteral {
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String,
store: _StorageProvider? = nil) where Value == String? {
init(wrappedValue: Value, _ key: String, store: _StorageProvider? = nil)
where Value == String?
{
defaultValue = wrappedValue
self.key = key
provider = store
@ -174,8 +176,9 @@ struct DefaultAppStorageEnvironmentKey: EnvironmentKey {
static let defaultValue: _StorageProvider? = nil
}
extension EnvironmentValues {
public var _defaultAppStorage: _StorageProvider? {
public extension EnvironmentValues {
@_spi(TokamakCore)
var _defaultAppStorage: _StorageProvider? {
get {
self[DefaultAppStorageEnvironmentKey.self]
}
@ -185,8 +188,8 @@ extension EnvironmentValues {
}
}
extension View {
public func defaultAppStorage(_ store: _StorageProvider) -> some View {
public extension View {
func defaultAppStorage(_ store: _StorageProvider) -> some View {
environment(\._defaultAppStorage, store)
}
}

View File

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

View File

@ -15,67 +15,280 @@
// Created by Carson Katri on 7/16/20.
//
@_functionBuilder
public struct SceneBuilder {
@resultBuilder
public enum SceneBuilder {
public static func buildBlock<Content: Scene>(_ content: Content) -> some Scene {
content
}
}
// swiftlint:disable line_length
// swiftlint:disable large_tuple
// swiftlint:disable function_parameter_count
extension SceneBuilder {
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene, C1: Scene {
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
public extension SceneBuilder {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
C1: Scene
{
_TupleScene(
(c0, c1),
children: [_AnyScene(c0), _AnyScene(c1)],
visit: {
$0.visit(c0)
$0.visit(c1)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene where C0: Scene, C1: Scene, C2: Scene {
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
where C0: Scene, C1: Scene, C2: Scene
{
_TupleScene(
(c0, c1, c2),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
_TupleScene((c0, c1, c2, c3), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
_TupleScene(
(c0, c1, c2, c3),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
_TupleScene((c0, c1, c2, c3, c4), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
_TupleScene(
(c0, c1, c2, c3, c4),
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene {
_TupleScene((c0, c1, c2, c3, c4, c5), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4, C5>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene,
C5: Scene
{
_TupleScene(
(c0, c1, c2, c3, c4, c5),
children: [
_AnyScene(c0),
_AnyScene(c1),
_AnyScene(c2),
_AnyScene(c3),
_AnyScene(c4),
_AnyScene(c5),
],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene {
_TupleScene((c0, c1, c2, c3, c4, c5, c6), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene,
C4: Scene, C5: Scene, C6: Scene
{
_TupleScene(
(c0, c1, c2, c3, c4, c5, c6),
children: [
_AnyScene(c0),
_AnyScene(c1),
_AnyScene(c2),
_AnyScene(c3),
_AnyScene(c4),
_AnyScene(c5),
_AnyScene(c6),
],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene {
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
C7: Scene
{
_TupleScene(
(c0, c1, c2, c3, c4, c5, c6, c7),
children: [
_AnyScene(c0),
_AnyScene(c1),
_AnyScene(c2),
_AnyScene(c3),
_AnyScene(c4),
_AnyScene(c5),
_AnyScene(c6),
_AnyScene(c7),
],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene, C8: Scene {
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7, c8), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7), _AnyScene(c8)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7,
_ c8: C8
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
C7: Scene, C8: Scene
{
_TupleScene(
(c0, c1, c2, c3, c4, c5, c6, c7, c8),
children: [
_AnyScene(c0),
_AnyScene(c1),
_AnyScene(c2),
_AnyScene(c3),
_AnyScene(c4),
_AnyScene(c5),
_AnyScene(c6),
_AnyScene(c7),
_AnyScene(c8),
],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
}
)
}
}
extension SceneBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene, C7: Scene, C8: Scene, C9: Scene {
_TupleScene((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4), _AnyScene(c5), _AnyScene(c6), _AnyScene(c7), _AnyScene(c8), _AnyScene(c9)])
public extension SceneBuilder {
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7,
_ c8: C8,
_ c9: C9
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene, C5: Scene, C6: Scene,
C7: Scene, C8: Scene, C9: Scene
{
_TupleScene(
(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9),
children: [
_AnyScene(c0),
_AnyScene(c1),
_AnyScene(c2),
_AnyScene(c3),
_AnyScene(c4),
_AnyScene(c5),
_AnyScene(c6),
_AnyScene(c7),
_AnyScene(c8),
_AnyScene(c9),
],
visit: {
$0.visit(c0)
$0.visit(c1)
$0.visit(c2)
$0.visit(c3)
$0.visit(c4)
$0.visit(c5)
$0.visit(c6)
$0.visit(c7)
$0.visit(c8)
$0.visit(c9)
}
)
}
}

View File

@ -25,8 +25,8 @@ struct ScenePhaseKey: EnvironmentKey {
static let defaultValue: ScenePhase = .active
}
extension EnvironmentValues {
public var scenePhase: ScenePhase {
public extension EnvironmentValues {
var scenePhase: ScenePhase {
get {
self[ScenePhaseKey.self]
}

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");
// you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/17/20.
//
import CombineShim
import OpenCombineShim
/// The renderer must specify a default `_StorageProvider` before any `SceneStorage`
/// values are accessed.
@ -23,7 +23,8 @@ public enum _DefaultSceneStorageProvider {
public static var `default`: _StorageProvider!
}
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
@propertyWrapper
public struct SceneStorage<Value>: DynamicProperty {
let key: String
let defaultValue: Value
let store: (_StorageProvider, String, Value) -> ()
@ -53,42 +54,38 @@ public enum _DefaultSceneStorageProvider {
extension SceneStorage: ObservedProperty {}
extension SceneStorage {
public init(wrappedValue: Value,
_ key: String) where Value == Bool {
public extension SceneStorage {
init(wrappedValue: Value, _ key: String) where Value == Bool {
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2) }
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String) where Value == Int {
init(wrappedValue: Value, _ key: String) where Value == Int {
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2) }
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String) where Value == Double {
init(wrappedValue: Value, _ key: String) where Value == Double {
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2) }
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String) where Value == String {
init(wrappedValue: Value, _ key: String) where Value == String {
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2) }
read = { $0.read(key: $1) }
}
public init(wrappedValue: Value,
_ key: String)
where Value: RawRepresentable, Value.RawValue == Int {
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable,
Value.RawValue == Int
{
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2.rawValue) }
@ -100,9 +97,9 @@ extension SceneStorage {
}
}
public init(wrappedValue: Value,
_ key: String)
where Value: RawRepresentable, Value.RawValue == String {
init(wrappedValue: Value, _ key: String)
where Value: RawRepresentable, Value.RawValue == String
{
defaultValue = wrappedValue
self.key = key
store = { $0.store(key: $1, value: $2.rawValue) }

View File

@ -27,18 +27,17 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
self.content = content()
}
@_disfavoredOverload public init(_ title: Text,
id: String,
@ViewBuilder content: () -> Content) {
@_disfavoredOverload
public init(_ title: Text, id: String, @ViewBuilder content: () -> Content) {
self.id = id
self.title = title
self.content = content()
}
@_disfavoredOverload public init<S>(_ title: S,
id: String,
@ViewBuilder content: () -> Content)
where S: StringProtocol {
@_disfavoredOverload
public init<S>(_ title: S, id: String, @ViewBuilder content: () -> Content)
where S: StringProtocol
{
self.id = id
self.title = Text(title)
self.content = content()
@ -50,21 +49,21 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
self.content = content()
}
@_disfavoredOverload public init(_ title: Text,
@ViewBuilder content: () -> Content) {
@_disfavoredOverload
public init(_ title: Text, @ViewBuilder content: () -> Content) {
id = ""
self.title = title
self.content = content()
}
@_disfavoredOverload public init<S>(_ title: S,
@ViewBuilder content: () -> Content)
where S: StringProtocol {
@_disfavoredOverload
public init<S>(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol {
id = ""
self.title = Text(title)
self.content = content()
}
@_spi(TokamakCore)
public var body: Never {
neverScene("WindowGroup")
}
@ -76,4 +75,8 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
// public init(_ titleKey: LocalizedStringKey,
// @ViewBuilder content: () -> Content) {
// }
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
visitor.visit(content)
}
}

View File

@ -25,6 +25,7 @@ public struct _SceneModifier_Content<Modifier>: Scene where Modifier: _SceneModi
public let modifier: Modifier
public let scene: _AnyScene
@_spi(TokamakCore)
public var body: Never {
neverScene("_SceneModifier_Content")
}
@ -36,8 +37,8 @@ public extension Scene {
}
}
extension _SceneModifier where Body == Never {
public func body(content: SceneContent) -> Body {
public extension _SceneModifier where Body == Never {
func body(content: SceneContent) -> Body {
fatalError("""
\(self) is a primitive `_SceneModifier`, you're not supposed to run `body(content:)`
""")

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");
// you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/19/20.
//
import CombineShim
import OpenCombineShim
public struct _AnyApp: App {
var app: Any
@ -23,7 +23,7 @@ public struct _AnyApp: App {
let bodyClosure: (Any) -> _AnyScene
let bodyType: Any.Type
init<A: App>(_ app: A) {
public init<A: App>(_ app: A) {
self.app = app
type = A.self
// swiftlint:disable:next force_cast
@ -31,24 +31,37 @@ public struct _AnyApp: App {
bodyType = A.Body.self
}
@_spi(TokamakCore)
public var body: Never {
neverScene("_AnyApp")
}
@_spi(TokamakCore)
public init() {
fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.")
}
public static func _launch(_ app: Self,
_ rootEnvironment: EnvironmentValues) {
@_spi(TokamakCore)
public static func _launch(_ app: Self, with configuration: _AppConfiguration) {
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
}
@_spi(TokamakCore)
public static func _setTitle(_ title: String) {
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
}
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
public static var _configuration: _AppConfiguration {
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
}
@_spi(TokamakCore)
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
}
@_spi(TokamakCore)
public var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> {
fatalError("`_AnyApp` cannot monitor colorScheme. Access underlying `app` value.")
}
}

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");
// you may not use this file except in compliance with the License.
@ -15,8 +15,6 @@
// Created by Carson Katri on 7/19/20.
//
import Runtime
public struct _AnyScene: Scene {
/** The result type of `bodyClosure` allowing to disambiguate between scenes that
produce other scenes or scenes that only produce containing views.
@ -63,16 +61,12 @@ public struct _AnyScene: Scene {
// swiftlint:disable:next force_cast
bodyClosure = { .scene(_AnyScene(($0 as! S).body)) }
}
// FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get
// a name of a type constructor in runtime. Should definitely check if these are different
// across modules, otherwise can cause problems with scenes with same names in different
// modules.
// swiftlint:disable:next force_try
typeConstructorName = try! typeInfo(of: type).mangledName
typeConstructorName = TokamakCore.typeConstructorName(type)
}
}
@_spi(TokamakCore)
public var body: Never {
neverScene("_AnyScene")
}

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");
// you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
// Created by Carson Katri on 7/22/20.
//
import CombineShim
import OpenCombineShim
public protocol _StorageProvider {
func store(key: String, value: Bool?)

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,43 +15,10 @@
// Created by Carson Katri on 7/17/20.
//
import Runtime
public protocol DynamicProperty {
mutating func update()
}
extension DynamicProperty {
public mutating func update() {}
}
extension TypeInfo {
/// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point.
func dynamicProperties(_ environment: EnvironmentValues,
source: inout Any,
shouldUpdate: Bool) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
// swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type)
propInfo.injectEnvironment(from: environment, into: &source)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(environment,
source: &extracted,
shouldUpdate: shouldUpdate)
)
// swiftlint:disable:next force_cast
var extractedDynamicProp = extracted as! DynamicProperty
if shouldUpdate {
extractedDynamicProp.update()
}
try! prop.set(value: extractedDynamicProp, on: &source)
// swiftlint:enable force_try
}
return dynamicProps
}
public extension DynamicProperty {
mutating func update() {}
}

View File

@ -23,14 +23,15 @@ protocol EnvironmentReader {
mutating func setContent(from values: EnvironmentValues)
}
@propertyWrapper public struct Environment<Value>: DynamicProperty {
@propertyWrapper
public struct Environment<Value>: DynamicProperty {
enum Content {
case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value)
}
var content: Content
let keyPath: KeyPath<EnvironmentValues, Value>
private var content: Content
private let keyPath: KeyPath<EnvironmentValues, Value>
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
content = .keyPath(keyPath)
self.keyPath = keyPath

View File

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

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");
// you may not use this file except in compliance with the License.
@ -15,11 +15,14 @@
// Created by Carson Katri on 7/7/20.
//
import CombineShim
import OpenCombineShim
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
where ObjectType: ObservableObject {
@dynamicMemberLookup public struct Wrapper {
@propertyWrapper
public struct EnvironmentObject<ObjectType>: DynamicProperty
where ObjectType: ObservableObject
{
@dynamicMemberLookup
public struct Wrapper {
internal let root: ObjectType
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
@ -70,8 +73,8 @@ extension ObservableObject {
}
}
extension View {
public func environmentObject<B>(_ bindable: B) -> some View where B: ObservableObject {
public extension View {
func environmentObject<B>(_ bindable: B) -> some View where B: ObservableObject {
environment(B.environmentStore, bindable)
}
}

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");
// you may not use this file except in compliance with the License.
@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import CombineShim
import OpenCombineShim
public struct EnvironmentValues: CustomStringConvertible {
public var description: String {
String(describing: values)
"EnvironmentValues: \(values.count)"
}
private var values: [ObjectIdentifier: Any] = [:]
@ -43,9 +43,40 @@ public struct EnvironmentValues: CustomStringConvertible {
values[bindable] = newValue
}
}
@_spi(TokamakCore)
public mutating func merge(_ other: Self?) {
if let other = other {
values.merge(other.values) { _, new in
new
}
}
}
@_spi(TokamakCore)
public func merging(_ other: Self?) -> Self {
var merged = self
merged.merge(other)
return merged
}
}
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
struct IsEnabledKey: EnvironmentKey {
static let defaultValue = true
}
public extension EnvironmentValues {
var isEnabled: Bool {
get {
self[IsEnabledKey.self]
}
set {
self[IsEnabledKey.self] = newValue
}
}
}
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
let environmentValues: EnvironmentValues
func body(content: Content) -> some View {
@ -57,8 +88,8 @@ struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
}
}
extension View {
public func environmentValues(_ values: EnvironmentValues) -> some View {
public extension View {
func environmentValues(_ values: EnvironmentValues) -> some View {
modifier(_EnvironmentValuesWritingModifier(environmentValues: values))
}
}

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

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
protocol AppearanceActionProtocol {
protocol AppearanceActionType {
var appear: (() -> ())? { get }
var disappear: (() -> ())? { get }
}
@ -21,26 +21,23 @@ protocol AppearanceActionProtocol {
struct _AppearanceActionModifier: ViewModifier {
var appear: (() -> ())?
var disappear: (() -> ())?
init(appear: (() -> ())? = nil, disappear: (() -> ())? = nil) {
self.appear = appear
self.disappear = disappear
}
typealias Body = Never
}
extension ModifiedContent: AppearanceActionProtocol
where Content: View, Modifier == _AppearanceActionModifier {
extension ModifiedContent: AppearanceActionType
where Content: View, Modifier == _AppearanceActionModifier
{
var appear: (() -> ())? { modifier.appear }
var disappear: (() -> ())? { modifier.disappear }
}
extension View {
public func onAppear(perform action: (() -> ())? = nil) -> some View {
public extension View {
func onAppear(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(appear: action))
}
public func onDisappear(perform action: (() -> ())? = nil) -> some View {
func onDisappear(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action))
}
}

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");
// you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
// Created by Carson Katri on 06/29/2020.
//
import Foundation
public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
public var shape: ClipShape
public var style: FillStyle
@ -27,20 +29,29 @@ public struct _ClipEffect<ClipShape>: ViewModifier where ClipShape: Shape {
public func body(content: Content) -> some View {
content
}
public var animatableData: ClipShape.AnimatableData {
get { shape.animatableData }
set { shape.animatableData = newValue }
}
}
extension View {
public func clipShape<S>(_ shape: S, style: FillStyle = FillStyle()) -> some View where S: Shape {
public extension View {
func clipShape<S>(_ shape: S, style: FillStyle = FillStyle()) -> some View where S: Shape {
modifier(_ClipEffect(shape: shape, style: style))
}
public func clipped(antialiased: Bool = false) -> some View {
clipShape(Rectangle(),
style: FillStyle(antialiased: antialiased))
func clipped(antialiased: Bool = false) -> some View {
clipShape(
Rectangle(),
style: FillStyle(antialiased: antialiased)
)
}
public func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View {
clipShape(RoundedRectangle(cornerRadius: radius),
style: FillStyle(antialiased: antialiased))
func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View {
clipShape(
RoundedRectangle(cornerRadius: radius),
style: FillStyle(antialiased: antialiased)
)
}
}

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");
// you may not use this file except in compliance with the License.
@ -15,8 +15,10 @@
// Created by Carson Katri on 7/3/20.
//
import Foundation
// FIXME: Make `Animatable`
public protocol GeometryEffect: ViewModifier {
public protocol GeometryEffect: Animatable, ViewModifier {
func effectValue(size: CGSize) -> ProjectionTransform
}

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");
// you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
// Created by Carson Katri on 7/3/20.
//
import Foundation
public struct _RotationEffect: GeometryEffect {
public var angle: Angle
public var anchor: UnitPoint
@ -25,16 +27,25 @@ public struct _RotationEffect: GeometryEffect {
}
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(CGAffineTransform.identity.rotated(by: angle.radians))
.init(CGAffineTransform.identity.rotated(by: CGFloat(angle.radians)))
}
public func body(content: Content) -> some View {
content
}
public var animatableData: AnimatablePair<Angle.AnimatableData, UnitPoint.AnimatableData> {
get {
.init(angle.animatableData, anchor.animatableData)
}
set {
(angle.animatableData, anchor.animatableData) = newValue[]
}
}
}
extension View {
public func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
public extension View {
func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
modifier(_RotationEffect(angle: angle, anchor: anchor))
}
}

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");
// you may not use this file except in compliance with the License.
@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
public struct _FlexFrameLayout: ViewModifier {
public let minWidth: CGFloat?
public let idealWidth: CGFloat?
@ -21,13 +23,25 @@ public struct _FlexFrameLayout: ViewModifier {
public let maxHeight: CGFloat?
public let alignment: Alignment
init(minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment) {
// These are special cases in SwiftUI, where the child
// will request the entire width/height of the parent.
public var fillWidth: Bool {
(minWidth == 0 || minWidth == nil) && maxWidth == .infinity
}
public var fillHeight: Bool {
(minHeight == 0 || minHeight == nil) && maxHeight == .infinity
}
init(
minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment
) {
self.minWidth = minWidth
self.idealWidth = idealWidth
self.maxWidth = maxWidth
@ -42,14 +56,20 @@ public struct _FlexFrameLayout: ViewModifier {
}
}
extension View {
public func frame(minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment = .center) -> some View {
extension _FlexFrameLayout: Animatable {
public typealias AnimatableData = EmptyAnimatableData
}
public extension View {
func frame(
minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment = .center
) -> some View {
func areInNondecreasingOrder(
_ min: CGFloat?, _ ideal: CGFloat?, _ max: CGFloat?
) -> Bool {
@ -59,8 +79,9 @@ extension View {
return min <= ideal && ideal <= max
}
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth)
|| !areInNondecreasingOrder(minHeight, idealHeight, maxHeight) {
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth) ||
!areInNondecreasingOrder(minHeight, idealHeight, maxHeight)
{
fatalError("Contradictory frame constraints specified.")
}
@ -71,6 +92,7 @@ extension View {
minHeight: minHeight,
idealHeight: idealHeight, maxHeight: maxHeight,
alignment: alignment
))
)
)
}
}

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");
// you may not use this file except in compliance with the License.
@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
public struct _FrameLayout: ViewModifier {
public let width: CGFloat?
public let height: CGFloat?
public let alignment: Alignment
init(width: CGFloat?,
height: CGFloat?,
alignment: Alignment) {
init(width: CGFloat?, height: CGFloat?, alignment: Alignment) {
self.width = width
self.height = height
self.alignment = alignment
@ -30,10 +30,16 @@ public struct _FrameLayout: ViewModifier {
}
}
extension View {
public func frame(width: CGFloat? = nil,
height: CGFloat? = nil,
alignment: Alignment = .center) -> some View {
extension _FrameLayout: Animatable {
public typealias AnimatableData = EmptyAnimatableData
}
public extension View {
func frame(
width: CGFloat? = nil,
height: CGFloat? = nil,
alignment: Alignment = .center
) -> some View {
modifier(_FrameLayout(width: width, height: height, alignment: alignment))
}
}

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

@ -0,0 +1,47 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// FIXME: these should have standalone implementations
public extension View {
@_spi(TokamakCore)
func _onMount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(appear: action))
}
@_spi(TokamakCore)
func _onUpdate(perform action: (() -> ())? = nil) -> some View {
modifier(_LifecycleActionModifier(update: action))
}
@_spi(TokamakCore)
func _onUnmount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action))
}
}
protocol LifecycleActionType {
var update: (() -> ())? { get }
}
struct _LifecycleActionModifier: ViewModifier {
var update: (() -> ())?
typealias Body = Never
}
extension ModifiedContent: LifecycleActionType
where Content: View, Modifier == _LifecycleActionModifier
{
var update: (() -> ())? { modifier.update }
}

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