Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
0ccb84afd5 |
|
@ -3,7 +3,7 @@ name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: carson-katri
|
assignees: MaxDesiatov
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -28,17 +28,17 @@ A clear and concise description of what you expected to happen.
|
||||||
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
If this is a layout/rendering issue, please provide screenshots for both Tokamak and SwiftUI that highlight the difference.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. macOS 12.4]
|
- OS: [e.g. macOS]
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser [e.g. chrome, safari]
|
||||||
- Version of the browser [e.g. 22]
|
- Version of the browser [e.g. 22]
|
||||||
- Version of Tokamak [e.g. 0.10.1]
|
- Version of Tokamak [e.g. 0.6.1]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone 6]
|
- Device: [e.g. iPhone6]
|
||||||
- OS: [e.g. iOS15.1]
|
- OS: [e.g. iOS8.1]
|
||||||
- Browser [e.g. stock browser, safari]
|
- Browser [e.g. stock browser, safari]
|
||||||
- Version of the browser [e.g. 22]
|
- Version of the browser [e.g. 22]
|
||||||
- Version of Tokamak [e.g. 0.10.1]
|
- Version of Tokamak [e.g. 0.6.1]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
|
@ -14,88 +14,76 @@ jobs:
|
||||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||||
with:
|
with:
|
||||||
shell-action: carton bundle --product TokamakDemo
|
shell-action: carton bundle --product TokamakDemo
|
||||||
- name: Check binary size
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
ls -la Bundle
|
|
||||||
ls -lh Bundle/*.wasm | awk '{printf "::warning file=Sources/TokamakDemo/main.swift,line=1,col=1::TokamakDemo Wasm is %s.",$5}'
|
|
||||||
|
|
||||||
swiftwasm_test:
|
swiftwasm_test_5_6:
|
||||||
runs-on: ubuntu-20.04
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- run: echo "${{ matrix.toolchain }}" > .swift-version
|
|
||||||
- uses: swiftwasm/swiftwasm-action@v5.6
|
- uses: swiftwasm/swiftwasm-action@v5.6
|
||||||
with:
|
with:
|
||||||
shell-action: carton test --environment node
|
shell-action: carton test
|
||||||
|
|
||||||
core_macos_build:
|
# Disabled until macos-12 is available on GitHub Actions, which is required for Xcode 13.3
|
||||||
runs-on: macos-12
|
# core_macos_build:
|
||||||
|
# runs-on: macos-11
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v2
|
# - uses: actions/checkout@v2
|
||||||
- name: Run the test suite on macOS, build the demo project for iOS
|
# - name: Run the test suite on macOS, build the demo project for iOS
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
set -ex
|
# set -ex
|
||||||
sudo xcode-select --switch /Applications/Xcode_13.4.app/Contents/Developer/
|
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
||||||
# avoid building unrelated products for testing by specifying the test product explicitly
|
# # avoid building unrelated products for testing by specifying the test product explicitly
|
||||||
swift build --product TokamakPackageTests
|
# swift build --product TokamakPackageTests
|
||||||
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
# `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
|
||||||
(cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
# (cp -r /var/folders/*/*/*/*Tests . ; exit 1)
|
||||||
|
|
||||||
rm -rf Sources/TokamakGTKCHelpers/*.c
|
# rm -rf Sources/TokamakGTKCHelpers/*.c
|
||||||
|
|
||||||
xcodebuild -version
|
# xcodebuild -version
|
||||||
|
|
||||||
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
# # Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
|
||||||
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
# # Disable macOS builds until Monterey is available on GHA.
|
||||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
# # xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
|
||||||
xcpretty --color
|
# # CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||||
|
# # xcpretty --color
|
||||||
|
|
||||||
cd "NativeDemo"
|
# cd "NativeDemo"
|
||||||
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
# xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
|
||||||
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
# CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
|
||||||
xcpretty --color
|
# xcpretty --color
|
||||||
cd ..
|
# cd ..
|
||||||
|
|
||||||
./benchmark.sh
|
# ./benchmark.sh
|
||||||
|
|
||||||
- name: Upload failed snapshots
|
# - name: Upload failed snapshots
|
||||||
uses: actions/upload-artifact@v2
|
# uses: actions/upload-artifact@v2
|
||||||
if: ${{ failure() }}
|
# if: ${{ failure() }}
|
||||||
with:
|
# with:
|
||||||
name: Failed snapshots
|
# name: Failed snapshots
|
||||||
path: '*Tests'
|
# path: '*Tests'
|
||||||
|
|
||||||
# FIXME: disabled due to build errors, to be investigated
|
# gtk_macos_build:
|
||||||
# gtk_macos_build:
|
# runs-on: macos-11
|
||||||
# runs-on: macos-12
|
|
||||||
#
|
# steps:
|
||||||
# steps:
|
# - uses: actions/checkout@v2
|
||||||
# - uses: actions/checkout@v2
|
# - name: Build the GTK renderer on macOS
|
||||||
# - name: Build the GTK renderer on macOS
|
# shell: bash
|
||||||
# shell: bash
|
# run: |
|
||||||
# run: |
|
# set -ex
|
||||||
# set -ex
|
# sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
|
||||||
# sudo xcode-select --switch /Applications/Xcode_13.4.1.app/Contents/Developer/
|
|
||||||
#
|
# brew install gtk+3
|
||||||
# brew install gtk+3
|
|
||||||
#
|
# make build
|
||||||
# make build
|
|
||||||
|
|
||||||
gtk_ubuntu_18_04_build:
|
gtk_ubuntu_18_04_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-5.7-bionic
|
image: swiftlang/swift:nightly-bionic
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -110,7 +98,7 @@ jobs:
|
||||||
gtk_ubuntu_20_04_build:
|
gtk_ubuntu_20_04_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-5.7-focal
|
image: swiftlang/swift:nightly-focal
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
|
@ -8,7 +8,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
codecov:
|
codecov:
|
||||||
container:
|
container:
|
||||||
image: swiftlang/swift:nightly-5.7-focal
|
image: swiftlang/swift:nightly-focal
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
- run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gtk+-3.0 libgtk+-3.0
|
||||||
|
|
|
@ -24,7 +24,6 @@ jobs:
|
||||||
dependencies,
|
dependencies,
|
||||||
documentation,
|
documentation,
|
||||||
enhancement,
|
enhancement,
|
||||||
Fiber,
|
|
||||||
refactor,
|
refactor,
|
||||||
SwiftUI compatibility,
|
SwiftUI compatibility,
|
||||||
test suite,
|
test suite,
|
||||||
|
|
|
@ -8,7 +8,5 @@
|
||||||
--maxwidth 100
|
--maxwidth 100
|
||||||
--wraparguments before-first
|
--wraparguments before-first
|
||||||
--funcattributes prev-line
|
--funcattributes prev-line
|
||||||
--typeattributes prev-line
|
|
||||||
--varattributes prev-line
|
|
||||||
--disable andOperator
|
--disable andOperator
|
||||||
--swiftversion 5.6
|
--swiftversion 5.4
|
||||||
|
|
|
@ -29,19 +29,6 @@ only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conj
|
||||||
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
|
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.
|
`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
|
### Coding Style
|
||||||
|
|
||||||
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and
|
||||||
|
|
|
@ -46,23 +46,23 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(
|
.package(
|
||||||
url: "https://gitlink.org.cn/dnrops/JavaScriptKit.git",
|
url: "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||||
from: "0.15.0"
|
from: "0.15.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://gitlink.org.cn/dnrops/OpenCombine.git",
|
url: "https://github.com/OpenCombine/OpenCombine.git",
|
||||||
from: "0.12.0"
|
from: "0.12.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://gitcode.net/dnrops/OpenCombineJS.git",
|
url: "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||||
from: "0.2.0"
|
from: "0.2.0"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://gitlink.org.cn/dnrops/swift-benchmark",
|
url: "https://github.com/google/swift-benchmark",
|
||||||
from: "0.1.2"
|
from: "0.1.2"
|
||||||
),
|
),
|
||||||
.package(
|
.package(
|
||||||
url: "https://gitlink.org.cn/dnrops/swift-snapshot-testing.git",
|
url: "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
||||||
from: "1.9.0"
|
from: "1.9.0"
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -135,7 +135,6 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Benchmark", package: "swift-benchmark"),
|
.product(name: "Benchmark", package: "swift-benchmark"),
|
||||||
"TokamakCore",
|
"TokamakCore",
|
||||||
"TokamakTestRenderer",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
|
@ -195,25 +194,6 @@ let package = Package(
|
||||||
name: "TokamakTestRenderer",
|
name: "TokamakTestRenderer",
|
||||||
dependencies: ["TokamakCore"]
|
dependencies: ["TokamakCore"]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "TokamakLayoutTests",
|
|
||||||
dependencies: [
|
|
||||||
"TokamakCore",
|
|
||||||
"TokamakStaticHTML",
|
|
||||||
.product(
|
|
||||||
name: "SnapshotTesting",
|
|
||||||
package: "swift-snapshot-testing",
|
|
||||||
condition: .when(platforms: [.macOS])
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "TokamakReconcilerTests",
|
|
||||||
dependencies: [
|
|
||||||
"TokamakCore",
|
|
||||||
"TokamakTestRenderer",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TokamakTests",
|
name: "TokamakTests",
|
||||||
dependencies: ["TokamakTestRenderer"]
|
dependencies: ["TokamakTestRenderer"]
|
||||||
|
|
37
README.md
37
README.md
|
@ -138,31 +138,6 @@ This way both [Semantic UI](https://semantic-ui.com/) styles and [moment.js](htt
|
||||||
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
localized date formatting (or any arbitrary style/script/font added that way) are available in your
|
||||||
app.
|
app.
|
||||||
|
|
||||||
### Fiber renderers
|
|
||||||
|
|
||||||
A new reconciler modeled after React's [Fiber reconciler](https://reactjs.org/docs/faq-internals.html#what-is-react-fiber)
|
|
||||||
is optionally available. It can provide faster updates and allow for larger View hierarchies.
|
|
||||||
It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
|
|
||||||
|
|
||||||
You can specify which reconciler to use in your `App`'s configuration:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
struct CounterApp: App {
|
|
||||||
static let _configuration: _AppConfiguration = .init(
|
|
||||||
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
|
|
||||||
reconciler: .fiber(useDynamicLayout: true)
|
|
||||||
)
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup("Counter Demo") {
|
|
||||||
Counter(count: 5, limit: 15)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> *Note*: Not all `View`s and `ViewModifier`s are supported by Fiber renderers yet.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### For app developers
|
### For app developers
|
||||||
|
@ -172,7 +147,7 @@ struct CounterApp: App {
|
||||||
and macOS at the same time.
|
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.
|
- [Swift 5.6 or later](https://swift.org/download/) and Ubuntu 18.04/20.04 if you'd like to use Linux.
|
||||||
Other Linux distributions are currently not supported.
|
Other Linux distributions are currently not supported.
|
||||||
- [`carton` 0.15.x](https://carton.dev) (carton is our build tool, see the ["Getting started" section](#getting-started) for installation steps)
|
- [`carton` 0.14.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
|
### For users of apps depending on Tokamak
|
||||||
|
|
||||||
|
@ -241,9 +216,6 @@ carton dev
|
||||||
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
You can also clone this repository and run `carton dev --product TokamakDemo` in its root
|
||||||
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
directory. This will build the demo app that shows almost all of the currently implemented APIs.
|
||||||
|
|
||||||
If you have any questions, pleaes check out the [FAQ](docs/FAQ.md) document, and/or join the
|
|
||||||
#tokamak channel on [the SwiftWasm Discord server](https://discord.gg/ashJW8T8yp).
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
By default, the DOM renderer will escape HTML control characters in `Text` views. If you wish
|
||||||
|
@ -325,10 +297,9 @@ appreciated and helps in maintaining the project.
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
In alphabetical order: [Carson Katri](https://github.com/carson-katri),
|
||||||
[Ezra Berch](https://github.com/ezraberch),
|
[David Hunt](https://github.com/foscomputerservices), [Ezra Berch](https://github.com/ezraberch),
|
||||||
[Jed Fox](https://jedfox.com),
|
[Jed Fox](https://jedfox.com), [Max Desiatov](https://desiatov.com),
|
||||||
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/),
|
[Morten Bek Ditlevsen](https://github.com/mortenbekditlevsen/), [Yuta Saito](https://github.com/kateinoigakukun/).
|
||||||
[Yuta Saito](https://github.com/kateinoigakukun/).
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
|
|
|
@ -40,20 +40,14 @@ public extension Animatable where Self.AnimatableData == EmptyAnimatableData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct EmptyAnimatableData: VectorArithmetic {
|
||||||
public struct EmptyAnimatableData: VectorArithmetic {
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public init() {}
|
public init() {}
|
||||||
|
@inlinable public static var zero: Self { .init() }
|
||||||
@inlinable
|
|
||||||
public static var zero: Self { .init() }
|
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func += (lhs: inout Self, rhs: Self) {}
|
public static func += (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func -= (lhs: inout Self, rhs: Self) {}
|
public static func -= (lhs: inout Self, rhs: Self) {}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||||
.zero
|
.zero
|
||||||
|
@ -66,15 +60,11 @@ public struct EmptyAnimatableData: VectorArithmetic {
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public mutating func scale(by rhs: Double) {}
|
public mutating func scale(by rhs: Double) {}
|
||||||
|
@inlinable public var magnitudeSquared: Double { .zero }
|
||||||
@inlinable
|
|
||||||
public var magnitudeSquared: Double { .zero }
|
|
||||||
|
|
||||||
public static func == (a: Self, b: Self) -> Bool { true }
|
public static func == (a: Self, b: Self) -> Bool { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||||
public struct AnimatablePair<First, Second>: VectorArithmetic
|
|
||||||
where First: VectorArithmetic, Second: VectorArithmetic
|
where First: VectorArithmetic, Second: VectorArithmetic
|
||||||
{
|
{
|
||||||
public var first: First
|
public var first: First
|
||||||
|
@ -91,8 +81,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||||
set { (first, second) = newValue }
|
set { (first, second) = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@_transparent
|
@_transparent public static var zero: Self {
|
||||||
public static var zero: Self {
|
|
||||||
@_transparent get {
|
@_transparent get {
|
||||||
.init(First.zero, Second.zero)
|
.init(First.zero, Second.zero)
|
||||||
}
|
}
|
||||||
|
@ -126,8 +115,7 @@ public struct AnimatablePair<First, Second>: VectorArithmetic
|
||||||
second.scale(by: rhs)
|
second.scale(by: rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@_transparent
|
@_transparent public var magnitudeSquared: Double {
|
||||||
public var magnitudeSquared: Double {
|
|
||||||
@_transparent get {
|
@_transparent get {
|
||||||
first.magnitudeSquared + second.magnitudeSquared
|
first.magnitudeSquared + second.magnitudeSquared
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,8 +139,7 @@ public struct _AnimationProxy {
|
||||||
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
|
public func resolve() -> _AnimationBoxBase._Resolved { subject.box.resolve() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||||
public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
|
||||||
where Value: Equatable
|
where Value: Equatable
|
||||||
{
|
{
|
||||||
public var animation: Animation?
|
public var animation: Animation?
|
||||||
|
@ -156,9 +155,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||||
let content: Content
|
let content: Content
|
||||||
let animation: Animation?
|
let animation: Animation?
|
||||||
let value: Value
|
let value: Value
|
||||||
|
@State private var lastValue: Value?
|
||||||
@State
|
|
||||||
private var lastValue: Value?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content.transaction {
|
content.transaction {
|
||||||
|
@ -183,8 +180,7 @@ public struct _AnimationModifier<Value>: ViewModifier, Equatable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _AnimationView<Content>: View
|
||||||
public struct _AnimationView<Content>: View
|
|
||||||
where Content: Equatable, Content: View
|
where Content: Equatable, Content: View
|
||||||
{
|
{
|
||||||
public var content: Content
|
public var content: Content
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
public struct Transaction {
|
public struct Transaction {
|
||||||
/// The overridden transaction for a state change in a `withTransaction` block.
|
/// The overriden transaction for a state change in a `withTransaction` block.
|
||||||
/// Is always set back to `nil` when the block exits.
|
/// Is always set back to `nil` when the block exits.
|
||||||
static var _active: Self?
|
static var _active: Self?
|
||||||
|
|
||||||
|
@ -50,8 +50,7 @@ protocol _TransactionModifierProtocol {
|
||||||
func modifyTransaction(_ transaction: inout Transaction)
|
func modifyTransaction(_ transaction: inout Transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _TransactionModifier: ViewModifier {
|
||||||
public struct _TransactionModifier: ViewModifier {
|
|
||||||
public var transform: (inout Transaction) -> ()
|
public var transform: (inout Transaction) -> ()
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
|
@ -78,8 +77,7 @@ extension ModifiedContent: _TransactionModifierProtocol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
|
||||||
public struct _PushPopTransactionModifier<V>: ViewModifier where V: ViewModifier {
|
|
||||||
public var content: V
|
public var content: V
|
||||||
public var base: _TransactionModifier
|
public var base: _TransactionModifier
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,7 @@ public protocol VectorArithmetic: AdditiveArithmetic {
|
||||||
extension Float: VectorArithmetic {
|
extension Float: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
|
public mutating func scale(by rhs: Double) { self *= Float(rhs) }
|
||||||
|
@_transparent public var magnitudeSquared: Double {
|
||||||
@_transparent
|
|
||||||
public var magnitudeSquared: Double {
|
|
||||||
@_transparent get { Double(self * self) }
|
@_transparent get { Double(self * self) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,9 +33,7 @@ extension Float: VectorArithmetic {
|
||||||
extension Double: VectorArithmetic {
|
extension Double: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= rhs }
|
public mutating func scale(by rhs: Double) { self *= rhs }
|
||||||
|
@_transparent public var magnitudeSquared: Double {
|
||||||
@_transparent
|
|
||||||
public var magnitudeSquared: Double {
|
|
||||||
@_transparent get { self * self }
|
@_transparent get { self * self }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +41,7 @@ extension Double: VectorArithmetic {
|
||||||
extension CGFloat: VectorArithmetic {
|
extension CGFloat: VectorArithmetic {
|
||||||
@_transparent
|
@_transparent
|
||||||
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
|
public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) }
|
||||||
|
@_transparent public var magnitudeSquared: Double {
|
||||||
@_transparent
|
|
||||||
public var magnitudeSquared: Double {
|
|
||||||
@_transparent get { Double(self * self) }
|
@_transparent get { Double(self * self) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import Foundation
|
||||||
public protocol _AnimationSolver {
|
public protocol _AnimationSolver {
|
||||||
/// Solve value at a specific point in time.
|
/// Solve value at a specific point in time.
|
||||||
func solve(at t: Double) -> Double
|
func solve(at t: Double) -> Double
|
||||||
/// Calculates the duration of the animation to a specific precision.
|
/// Calculates the duration of the animation to a specific presision.
|
||||||
func restingPoint(precision y: Double) -> Double
|
func restingPoint(precision y: Double) -> Double
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
// 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 ?? ()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,8 +20,7 @@ import Foundation
|
||||||
public protocol _VectorMath: Animatable {}
|
public protocol _VectorMath: Animatable {}
|
||||||
|
|
||||||
public extension _VectorMath {
|
public extension _VectorMath {
|
||||||
@inlinable
|
@inlinable var magnitude: Double {
|
||||||
var magnitude: Double {
|
|
||||||
animatableData.magnitudeSquared.squareRoot()
|
animatableData.magnitudeSquared.squareRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,7 @@ public protocol App: _TitledApp {
|
||||||
var body: Body { get }
|
var body: Body { get }
|
||||||
|
|
||||||
/// Implemented by the renderer to mount the `App`
|
/// Implemented by the renderer to mount the `App`
|
||||||
static func _launch(
|
static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues)
|
||||||
_ app: Self,
|
|
||||||
with configuration: _AppConfiguration
|
|
||||||
)
|
|
||||||
|
|
||||||
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
/// Implemented by the renderer to update the `App` on `ScenePhase` changes
|
||||||
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
var _phasePublisher: AnyPublisher<ScenePhase, Never> { get }
|
||||||
|
@ -39,38 +36,14 @@ public protocol App: _TitledApp {
|
||||||
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
/// Implemented by the renderer to update the `App` on `ColorScheme` changes
|
||||||
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
var _colorSchemePublisher: AnyPublisher<ColorScheme, Never> { get }
|
||||||
|
|
||||||
static var _configuration: _AppConfiguration { get }
|
|
||||||
|
|
||||||
static func main()
|
static func main()
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
public 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 {
|
public extension App {
|
||||||
static var _configuration: _AppConfiguration { .init() }
|
|
||||||
|
|
||||||
static func main() {
|
static func main() {
|
||||||
let app = Self()
|
let app = Self()
|
||||||
_launch(app, with: Self._configuration)
|
_launch(app, EnvironmentValues())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,9 @@
|
||||||
|
|
||||||
import OpenCombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper public struct AppStorage<Value>: DynamicProperty {
|
||||||
public struct AppStorage<Value>: DynamicProperty {
|
|
||||||
let provider: _StorageProvider?
|
let provider: _StorageProvider?
|
||||||
|
@Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider?
|
||||||
@Environment(\._defaultAppStorage)
|
|
||||||
var defaultProvider: _StorageProvider?
|
|
||||||
|
|
||||||
var unwrappedProvider: _StorageProvider {
|
var unwrappedProvider: _StorageProvider {
|
||||||
provider ?? defaultProvider!
|
provider ?? defaultProvider!
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,23 +21,8 @@ public protocol Scene {
|
||||||
// FIXME: If I put `@SceneBuilder` in front of this
|
// FIXME: If I put `@SceneBuilder` in front of this
|
||||||
// it fails to build with no useful error message.
|
// it fails to build with no useful error message.
|
||||||
var body: Self.Body { get }
|
var body: Self.Body { get }
|
||||||
|
|
||||||
/// Override the default implementation for `Scene`s with body types of `Never`
|
|
||||||
/// or in cases where the body would normally need to be type erased.
|
|
||||||
///
|
|
||||||
/// You can `visit(_:)` either another `Scene` or a `View` with a `SceneVisitor`
|
|
||||||
func _visitChildren<V: SceneVisitor>(_ visitor: V)
|
|
||||||
|
|
||||||
/// Create `SceneOutputs`, including any modifications to the environment, preferences, or a custom
|
|
||||||
/// `LayoutComputer` from the `SceneInputs`.
|
|
||||||
///
|
|
||||||
/// > At the moment, `SceneInputs`/`SceneOutputs` are identical to `ViewInputs`/`ViewOutputs`.
|
|
||||||
static func _makeScene(_ inputs: SceneInputs<Self>) -> SceneOutputs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public typealias SceneInputs<S: Scene> = ViewInputs<S>
|
|
||||||
public typealias SceneOutputs = ViewOutputs
|
|
||||||
|
|
||||||
protocol TitledScene {
|
protocol TitledScene {
|
||||||
var title: Text? { get }
|
var title: Text? { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,14 +29,7 @@ public extension SceneBuilder {
|
||||||
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> some Scene where C0: Scene,
|
||||||
C1: Scene
|
C1: Scene
|
||||||
{
|
{
|
||||||
_TupleScene(
|
_TupleScene((c0, c1), children: [_AnyScene(c0), _AnyScene(c1)])
|
||||||
(c0, c1),
|
|
||||||
children: [_AnyScene(c0), _AnyScene(c1)],
|
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,15 +37,7 @@ public extension SceneBuilder {
|
||||||
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> some Scene
|
||||||
where C0: Scene, C1: Scene, C2: Scene
|
where C0: Scene, C1: Scene, C2: Scene
|
||||||
{
|
{
|
||||||
_TupleScene(
|
_TupleScene((c0, c1, c2), children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)])
|
||||||
(c0, c1, c2),
|
|
||||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2)],
|
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,13 +50,7 @@ public extension SceneBuilder {
|
||||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene {
|
||||||
_TupleScene(
|
_TupleScene(
|
||||||
(c0, c1, c2, c3),
|
(c0, c1, c2, c3),
|
||||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)],
|
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3)]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,14 +65,7 @@ public extension SceneBuilder {
|
||||||
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
) -> some Scene where C0: Scene, C1: Scene, C2: Scene, C3: Scene, C4: Scene {
|
||||||
_TupleScene(
|
_TupleScene(
|
||||||
(c0, c1, c2, c3, c4),
|
(c0, c1, c2, c3, c4),
|
||||||
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)],
|
children: [_AnyScene(c0), _AnyScene(c1), _AnyScene(c2), _AnyScene(c3), _AnyScene(c4)]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,15 +90,7 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c3),
|
_AnyScene(c3),
|
||||||
_AnyScene(c4),
|
_AnyScene(c4),
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
],
|
]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
$0.visit(c5)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,16 +117,7 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c4),
|
_AnyScene(c4),
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
],
|
]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
$0.visit(c5)
|
|
||||||
$0.visit(c6)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,17 +146,7 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c5),
|
_AnyScene(c5),
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
],
|
]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
$0.visit(c5)
|
|
||||||
$0.visit(c6)
|
|
||||||
$0.visit(c7)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,18 +177,7 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c6),
|
_AnyScene(c6),
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
_AnyScene(c8),
|
_AnyScene(c8),
|
||||||
],
|
]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
$0.visit(c5)
|
|
||||||
$0.visit(c6)
|
|
||||||
$0.visit(c7)
|
|
||||||
$0.visit(c8)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,19 +210,7 @@ public extension SceneBuilder {
|
||||||
_AnyScene(c7),
|
_AnyScene(c7),
|
||||||
_AnyScene(c8),
|
_AnyScene(c8),
|
||||||
_AnyScene(c9),
|
_AnyScene(c9),
|
||||||
],
|
]
|
||||||
visit: {
|
|
||||||
$0.visit(c0)
|
|
||||||
$0.visit(c1)
|
|
||||||
$0.visit(c2)
|
|
||||||
$0.visit(c3)
|
|
||||||
$0.visit(c4)
|
|
||||||
$0.visit(c5)
|
|
||||||
$0.visit(c6)
|
|
||||||
$0.visit(c7)
|
|
||||||
$0.visit(c8)
|
|
||||||
$0.visit(c9)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,7 @@ public enum _DefaultSceneStorageProvider {
|
||||||
public static var `default`: _StorageProvider!
|
public static var `default`: _StorageProvider!
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper public struct SceneStorage<Value>: DynamicProperty {
|
||||||
public struct SceneStorage<Value>: DynamicProperty {
|
|
||||||
let key: String
|
let key: String
|
||||||
let defaultValue: Value
|
let defaultValue: Value
|
||||||
let store: (_StorageProvider, String, Value) -> ()
|
let store: (_StorageProvider, String, Value) -> ()
|
||||||
|
|
|
@ -75,8 +75,4 @@ public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
|
||||||
// public init(_ titleKey: LocalizedStringKey,
|
// public init(_ titleKey: LocalizedStringKey,
|
||||||
// @ViewBuilder content: () -> Content) {
|
// @ViewBuilder content: () -> Content) {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
public func _visitChildren<V>(_ visitor: V) where V: SceneVisitor {
|
|
||||||
visitor.visit(content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ public struct _AnyApp: App {
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public static func _launch(_ app: Self, with configuration: _AppConfiguration) {
|
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
|
||||||
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
|
fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +51,6 @@ public struct _AnyApp: App {
|
||||||
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var _configuration: _AppConfiguration {
|
|
||||||
fatalError("`configuration` cannot be set for `AnyApp`. Access underlying `app` value.")
|
|
||||||
}
|
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
public var _phasePublisher: AnyPublisher<ScenePhase, Never> {
|
||||||
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.")
|
||||||
|
|
|
@ -17,17 +17,11 @@
|
||||||
|
|
||||||
struct _TupleScene<T>: Scene, GroupScene {
|
struct _TupleScene<T>: Scene, GroupScene {
|
||||||
let value: T
|
let value: T
|
||||||
let children: [_AnyScene]
|
var children: [_AnyScene]
|
||||||
let visit: (SceneVisitor) -> ()
|
|
||||||
|
|
||||||
init(
|
init(_ value: T, children: [_AnyScene]) {
|
||||||
_ value: T,
|
|
||||||
children: [_AnyScene],
|
|
||||||
visit: @escaping (SceneVisitor) -> ()
|
|
||||||
) {
|
|
||||||
self.value = value
|
self.value = value
|
||||||
self.children = children
|
self.children = children
|
||||||
self.visit = visit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: Never {
|
var body: Never {
|
||||||
|
|
|
@ -23,8 +23,7 @@ protocol EnvironmentReader {
|
||||||
mutating func setContent(from values: EnvironmentValues)
|
mutating func setContent(from values: EnvironmentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper public struct Environment<Value>: DynamicProperty {
|
||||||
public struct Environment<Value>: DynamicProperty {
|
|
||||||
enum Content {
|
enum Content {
|
||||||
case keyPath(KeyPath<EnvironmentValues, Value>)
|
case keyPath(KeyPath<EnvironmentValues, Value>)
|
||||||
case value(Value)
|
case value(Value)
|
||||||
|
|
|
@ -17,24 +17,11 @@ public protocol EnvironmentKey {
|
||||||
static var defaultValue: Value { get }
|
static var defaultValue: Value { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This protocol defines a type which mutates the environment in some way.
|
protocol EnvironmentModifier {
|
||||||
/// Unlike `EnvironmentalModifier`, which reads the environment to
|
|
||||||
/// create a `ViewModifier`.
|
|
||||||
///
|
|
||||||
/// It can be applied to a `View` or `ViewModifier`.
|
|
||||||
public protocol _EnvironmentModifier {
|
|
||||||
func modifyEnvironment(_ values: inout EnvironmentValues)
|
func modifyEnvironment(_ values: inout EnvironmentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ViewModifier where Self: _EnvironmentModifier {
|
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
|
||||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
var environment = inputs.environment.environment
|
|
||||||
inputs.content.modifyEnvironment(&environment)
|
|
||||||
return .init(inputs: inputs, environment: environment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentModifier {
|
|
||||||
public let keyPath: WritableKeyPath<EnvironmentValues, Value>
|
public let keyPath: WritableKeyPath<EnvironmentValues, Value>
|
||||||
public let value: Value
|
public let value: Value
|
||||||
|
|
||||||
|
@ -45,7 +32,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, _EnvironmentM
|
||||||
|
|
||||||
public typealias Body = Never
|
public typealias Body = Never
|
||||||
|
|
||||||
public func modifyEnvironment(_ values: inout EnvironmentValues) {
|
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||||
values[keyPath: keyPath] = value
|
values[keyPath: keyPath] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,10 @@
|
||||||
|
|
||||||
import OpenCombineShim
|
import OpenCombineShim
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper public struct EnvironmentObject<ObjectType>: DynamicProperty
|
||||||
public struct EnvironmentObject<ObjectType>: DynamicProperty
|
|
||||||
where ObjectType: ObservableObject
|
where ObjectType: ObservableObject
|
||||||
{
|
{
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup public struct Wrapper {
|
||||||
public struct Wrapper {
|
|
||||||
internal let root: ObjectType
|
internal let root: ObjectType
|
||||||
public subscript<Subject>(
|
public subscript<Subject>(
|
||||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||||
|
|
|
@ -76,7 +76,7 @@ public extension EnvironmentValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct _EnvironmentValuesWritingModifier: ViewModifier, _EnvironmentModifier {
|
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
||||||
let environmentValues: EnvironmentValues
|
let environmentValues: EnvironmentValues
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
// 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) ?? "")
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,626 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,290 +0,0 @@
|
||||||
// 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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
// 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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
// 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 }
|
|
||||||
}
|
|
|
@ -1,477 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,252 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,268 +0,0 @@
|
||||||
// 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) }
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
// 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() }
|
|
||||||
}
|
|
|
@ -1,296 +0,0 @@
|
||||||
// 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() }
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
// 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>
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
// 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)]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
// 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>
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
// 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!
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,8 +14,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public enum ContentMode: Hashable, CaseIterable {
|
||||||
public enum ContentMode: Hashable, CaseIterable {
|
|
||||||
case fit
|
case fit
|
||||||
case fill
|
case fill
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _OffsetEffect: GeometryEffect, Equatable {
|
||||||
public struct _OffsetEffect: GeometryEffect, Equatable {
|
|
||||||
public var offset: CGSize
|
public var offset: CGSize
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _ScaleEffect: GeometryEffect, Equatable {
|
||||||
public struct _ScaleEffect: GeometryEffect, Equatable {
|
|
||||||
public var scale: CGSize
|
public var scale: CGSize
|
||||||
public var anchor: UnitPoint
|
public var anchor: UnitPoint
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,14 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
protocol ModifierContainer {
|
protocol ModifierContainer {
|
||||||
var environmentModifier: _EnvironmentModifier? { get }
|
var environmentModifier: EnvironmentModifier? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ModifiedContentProtocol {}
|
protocol ModifiedContentProtocol {}
|
||||||
|
|
||||||
/// A value with a modifier applied to it.
|
/// A value with a modifier applied to it.
|
||||||
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||||
@Environment(\.self)
|
@Environment(\.self) public var environment
|
||||||
public var environment
|
|
||||||
|
|
||||||
public typealias Body = Never
|
public typealias Body = Never
|
||||||
public private(set) var content: Content
|
public private(set) var content: Content
|
||||||
public private(set) var modifier: Modifier
|
public private(set) var modifier: Modifier
|
||||||
|
@ -34,7 +32,7 @@ public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: ModifierContainer {
|
extension ModifiedContent: ModifierContainer {
|
||||||
var environmentModifier: _EnvironmentModifier? { modifier as? _EnvironmentModifier }
|
var environmentModifier: EnvironmentModifier? { modifier as? EnvironmentModifier }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
||||||
|
|
|
@ -24,18 +24,6 @@ public struct _BackgroundLayout<Content, Background>: _PrimitiveView
|
||||||
public let content: Content
|
public let content: Content
|
||||||
public let background: Background
|
public let background: Background
|
||||||
public let alignment: Alignment
|
public let alignment: Alignment
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
|
||||||
public init(content: Content, background: Background, alignment: Alignment) {
|
|
||||||
self.content = content
|
|
||||||
self.background = background
|
|
||||||
self.alignment = alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
|
||||||
visitor.visit(background)
|
|
||||||
visitor.visit(content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||||
|
@ -89,8 +77,7 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
||||||
public struct _BackgroundShapeModifier<Style, Bounds>: ViewModifier, EnvironmentReader
|
|
||||||
where Style: ShapeStyle, Bounds: Shape
|
where Style: ShapeStyle, Bounds: Shape
|
||||||
{
|
{
|
||||||
public var environment: EnvironmentValues!
|
public var environment: EnvironmentValues!
|
||||||
|
@ -142,11 +129,6 @@ public struct _OverlayLayout<Content, Overlay>: _PrimitiveView
|
||||||
public let content: Content
|
public let content: Content
|
||||||
public let overlay: Overlay
|
public let overlay: Overlay
|
||||||
public let alignment: Alignment
|
public let alignment: Alignment
|
||||||
|
|
||||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
|
||||||
visitor.visit(content)
|
|
||||||
visitor.visit(overlay)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||||
|
|
|
@ -16,23 +16,6 @@ public protocol ViewModifier {
|
||||||
typealias Content = _ViewModifier_Content<Self>
|
typealias Content = _ViewModifier_Content<Self>
|
||||||
associatedtype Body: View
|
associatedtype Body: View
|
||||||
func body(content: Content) -> Self.Body
|
func body(content: Content) -> Self.Body
|
||||||
|
|
||||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs
|
|
||||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension ViewModifier {
|
|
||||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
.init(inputs: inputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _visitChildren<V>(_ visitor: V, content: Content) where V: ViewVisitor {
|
|
||||||
if Body.self == Never.self {
|
|
||||||
content.visitChildren(visitor)
|
|
||||||
} else {
|
|
||||||
visitor.visit(body(content: content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _ViewModifier_Content<Modifier>: View
|
public struct _ViewModifier_Content<Modifier>: View
|
||||||
|
@ -40,27 +23,15 @@ public struct _ViewModifier_Content<Modifier>: View
|
||||||
{
|
{
|
||||||
public let modifier: Modifier
|
public let modifier: Modifier
|
||||||
public let view: AnyView
|
public let view: AnyView
|
||||||
let visitChildren: (ViewVisitor) -> ()
|
|
||||||
|
|
||||||
public init(modifier: Modifier, view: AnyView) {
|
public init(modifier: Modifier, view: AnyView) {
|
||||||
self.modifier = modifier
|
self.modifier = modifier
|
||||||
self.view = view
|
self.view = view
|
||||||
visitChildren = { $0.visit(view) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public init<V: View>(modifier: Modifier, view: V) {
|
|
||||||
self.modifier = modifier
|
|
||||||
self.view = AnyView(view)
|
|
||||||
visitChildren = { $0.visit(view) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
|
||||||
visitChildren(visitor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -78,6 +78,9 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
|
|
||||||
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
|
||||||
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
|
||||||
|
if let parent = parent {
|
||||||
|
parent.preferenceStore.merge(with: self.preferenceStore)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
|
||||||
|
|
|
@ -94,9 +94,13 @@ public class MountedElement<R: Renderer> {
|
||||||
|
|
||||||
public internal(set) var environmentValues: EnvironmentValues
|
public internal(set) var environmentValues: EnvironmentValues
|
||||||
|
|
||||||
private(set) weak var parent: MountedElement<R>?
|
weak var parent: MountedElement<R>?
|
||||||
|
/// `didSet` on this field propagates the preference changes up the view tree.
|
||||||
var preferenceStore: _PreferenceStore = .init()
|
var preferenceStore: _PreferenceStore = .init() {
|
||||||
|
didSet {
|
||||||
|
parent?.preferenceStore.merge(with: preferenceStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public internal(set) var viewTraits: _ViewTraitStore
|
public internal(set) var viewTraits: _ViewTraitStore
|
||||||
|
|
||||||
|
@ -106,7 +110,6 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
viewTraits = .init()
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
connectParentPreferenceStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
|
@ -115,7 +118,6 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
viewTraits = .init()
|
viewTraits = .init()
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
connectParentPreferenceStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
@ -129,7 +131,6 @@ public class MountedElement<R: Renderer> {
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
self.viewTraits = viewTraits
|
self.viewTraits = viewTraits
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
connectParentPreferenceStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateEnvironment() {
|
func updateEnvironment() {
|
||||||
|
@ -144,10 +145,6 @@ public class MountedElement<R: Renderer> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectParentPreferenceStore() {
|
|
||||||
preferenceStore.parent = parent?.preferenceStore
|
|
||||||
}
|
|
||||||
|
|
||||||
/// You must call `super.prepareForMount` before all other mounting work.
|
/// You must call `super.prepareForMount` before all other mounting work.
|
||||||
func prepareForMount(with transaction: Transaction) {
|
func prepareForMount(with transaction: Transaction) {
|
||||||
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||||
|
|
|
@ -25,41 +25,12 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
|
||||||
static var defaultValue: Value { nil }
|
static var defaultValue: Value { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _PreferenceValueStorage: CustomDebugStringConvertible {
|
|
||||||
/// Every value the `Key` has had.
|
|
||||||
var valueList: [Any]
|
|
||||||
|
|
||||||
var debugDescription: String {
|
|
||||||
valueList.debugDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
init<Key: PreferenceKey>(_ key: Key.Type = Key.self) {
|
|
||||||
valueList = []
|
|
||||||
}
|
|
||||||
|
|
||||||
init(valueList: [Any]) {
|
|
||||||
self.valueList = valueList
|
|
||||||
}
|
|
||||||
|
|
||||||
func merge(_ other: _PreferenceValueStorage) {
|
|
||||||
valueList.append(contentsOf: other.valueList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reset() {
|
|
||||||
valueList = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct _PreferenceValue<Key> where Key: PreferenceKey {
|
public struct _PreferenceValue<Key> where Key: PreferenceKey {
|
||||||
var storage: _PreferenceValueStorage
|
/// Every value the `Key` has had.
|
||||||
|
var valueList: [Key.Value]
|
||||||
init(storage: _PreferenceValueStorage) {
|
|
||||||
self.storage = storage
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The latest value.
|
/// The latest value.
|
||||||
public var value: Key.Value {
|
public var value: Key.Value {
|
||||||
reduce(storage.valueList.compactMap { $0 as? Key.Value })
|
reduce(valueList)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reduce(_ values: [Key.Value]) -> Key.Value {
|
func reduce(_ values: [Key.Value]) -> Key.Value {
|
||||||
|
@ -77,79 +48,38 @@ public extension _PreferenceValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class _PreferenceStore: CustomDebugStringConvertible {
|
public struct _PreferenceStore {
|
||||||
/// The values of the `_PreferenceStore` on the last update.
|
|
||||||
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
|
|
||||||
/// The backing values of the `_PreferenceStore`.
|
/// The backing values of the `_PreferenceStore`.
|
||||||
private var values: [ObjectIdentifier: _PreferenceValueStorage]
|
private var values: [String: Any]
|
||||||
|
|
||||||
weak var parent: _PreferenceStore?
|
public init(values: [String: Any] = [:]) {
|
||||||
|
|
||||||
public var debugDescription: String {
|
|
||||||
"Preferences (\(ObjectIdentifier(self))): \(values)"
|
|
||||||
}
|
|
||||||
|
|
||||||
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
|
|
||||||
previousValues = [:]
|
|
||||||
self.values = values
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a late-binding token for `key`, or save the default value if it does not yet exist.
|
|
||||||
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
let keyID = ObjectIdentifier(key)
|
values[String(reflecting: key)] as? _PreferenceValue<Key>
|
||||||
let storage: _PreferenceValueStorage
|
?? _PreferenceValue(valueList: [Key.defaultValue])
|
||||||
if let existing = values[keyID] {
|
|
||||||
storage = existing
|
|
||||||
} else {
|
|
||||||
storage = .init(key)
|
|
||||||
values[keyID] = storage
|
|
||||||
}
|
|
||||||
return _PreferenceValue(storage: storage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the value `Key` had on the last update.
|
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||||
///
|
|
||||||
/// Used to check if the value changed during the last update.
|
|
||||||
func previousValue<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key))
|
let previousValues = self.value(forKey: key).valueList
|
||||||
|
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
public mutating func merge(with other: Self) {
|
||||||
where Key: PreferenceKey
|
self = merging(with: other)
|
||||||
{
|
|
||||||
let keyID = ObjectIdentifier(key)
|
|
||||||
if !values.keys.contains(keyID) {
|
|
||||||
values[keyID] = .init(key)
|
|
||||||
}
|
|
||||||
values[keyID]?.valueList.append(value)
|
|
||||||
parent?.insert(value, forKey: key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func merge(_ other: _PreferenceStore) {
|
public func merging(with other: Self) -> Self {
|
||||||
for (key, otherStorage) in other.values {
|
var result = values
|
||||||
if let storage = values[key] {
|
for (key, value) in other.values {
|
||||||
storage.merge(otherStorage)
|
result[key] = value
|
||||||
} else {
|
|
||||||
values[key] = .init(valueList: otherStorage.valueList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copies `values` to `previousValues`, and clears `values`.
|
|
||||||
///
|
|
||||||
/// Each reconcile pass the preferences are collected from scratch, so we need to
|
|
||||||
/// clear out the old values.
|
|
||||||
func reset() {
|
|
||||||
previousValues = values.mapValues {
|
|
||||||
_PreferenceValueStorage(valueList: $0.valueList)
|
|
||||||
}
|
|
||||||
for storage in values.values {
|
|
||||||
storage.reset()
|
|
||||||
}
|
}
|
||||||
|
return .init(values: result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,26 +25,12 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
|
||||||
|
|
||||||
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
||||||
let value = preferenceStore.value(forKey: Key.self)
|
let value = preferenceStore.value(forKey: Key.self)
|
||||||
let previousValue = value.reduce((value.storage.valueList as? [Key.Value] ?? []).dropLast())
|
let previousValue = value.reduce(value.valueList.dropLast())
|
||||||
if previousValue != value.value {
|
if previousValue != value.value {
|
||||||
action(value.value)
|
action(value.value)
|
||||||
}
|
}
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
.init(
|
|
||||||
inputs: inputs,
|
|
||||||
preferenceStore: inputs.preferenceStore ?? .init(),
|
|
||||||
preferenceAction: {
|
|
||||||
let value = $0.value(forKey: Key.self).value
|
|
||||||
let previousValue = $0.previousValue(forKey: Key.self).value
|
|
||||||
if value != previousValue {
|
|
||||||
inputs.content.action(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -20,12 +20,11 @@
|
||||||
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
|
public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingViewProtocol
|
||||||
where Key: PreferenceKey, Content: View
|
where Key: PreferenceKey, Content: View
|
||||||
{
|
{
|
||||||
@State
|
@State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
|
||||||
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self))
|
valueList: [Key.defaultValue]
|
||||||
|
)
|
||||||
public let transform: (_PreferenceValue<Key>) -> Content
|
public let transform: (_PreferenceValue<Key>) -> Content
|
||||||
|
|
||||||
private var valueReference: _PreferenceValue<Key>?
|
|
||||||
|
|
||||||
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
||||||
self.transform = transform
|
self.transform = transform
|
||||||
}
|
}
|
||||||
|
@ -35,18 +34,7 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
transform(valueReference ?? resolvedValue)
|
transform(resolvedValue)
|
||||||
}
|
|
||||||
|
|
||||||
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
let preferenceStore = inputs.preferenceStore ?? .init()
|
|
||||||
inputs.updateContent {
|
|
||||||
$0.valueReference = preferenceStore.value(forKey: Key.self)
|
|
||||||
}
|
|
||||||
return .init(
|
|
||||||
inputs: inputs,
|
|
||||||
preferenceStore: preferenceStore
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +69,7 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.overlay($0._force(transform)) }
|
Key._delay { self.overlay(transform($0.value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func backgroundPreferenceValue<Key, T>(
|
func backgroundPreferenceValue<Key, T>(
|
||||||
|
@ -90,6 +78,6 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.background($0._force(transform)) }
|
Key._delay { self.background(transform($0.value)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,18 +34,6 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
|
||||||
preferenceStore.insert(newValue, forKey: Key.self)
|
preferenceStore.insert(newValue, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
.init(
|
|
||||||
inputs: inputs,
|
|
||||||
preferenceStore: inputs.preferenceStore ?? .init(),
|
|
||||||
preferenceAction: {
|
|
||||||
var value = $0.value(forKey: Key.self).value
|
|
||||||
inputs.content.transform(&value)
|
|
||||||
$0.insert(value, forKey: Key.self)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -27,14 +27,6 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
|
||||||
preferenceStore.insert(value, forKey: Key.self)
|
preferenceStore.insert(value, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
|
||||||
.init(
|
|
||||||
inputs: inputs,
|
|
||||||
preferenceStore: inputs.preferenceStore ?? .init(),
|
|
||||||
preferenceAction: { $0.insert(inputs.content.value, forKey: Key.self) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
||||||
|
|
|
@ -18,8 +18,7 @@ import Foundation
|
||||||
public protocol PreviewProvider {
|
public protocol PreviewProvider {
|
||||||
associatedtype Previews: View
|
associatedtype Previews: View
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder static var previews: Previews { get }
|
||||||
static var previews: Previews { get }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {
|
public struct PreviewDevice: RawRepresentable, ExpressibleByStringLiteral {
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
typealias FieldTypeAccessor = @convention(c)
|
typealias FieldTypeAccessor = @convention(c) (UnsafePointer<Int>) -> UnsafePointer<Int>
|
||||||
(UnsafePointer<Int>) -> UnsafePointer<Int>
|
|
||||||
|
|
||||||
struct StructTypeDescriptor {
|
struct StructTypeDescriptor {
|
||||||
let flags: Int32
|
let flags: Int32
|
||||||
|
|
|
@ -20,21 +20,7 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
public struct PropertyInfo: Hashable {
|
public struct PropertyInfo {
|
||||||
// Hashable/Equatable conformance is not synthesize for metatypes.
|
|
||||||
public static func == (lhs: PropertyInfo, rhs: PropertyInfo) -> Bool {
|
|
||||||
lhs.name == rhs.name && lhs.type == rhs.type && lhs.isVar == rhs.isVar && lhs.offset == rhs
|
|
||||||
.offset && lhs.ownerType == rhs.ownerType
|
|
||||||
}
|
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(name)
|
|
||||||
hasher.combine(ObjectIdentifier(type))
|
|
||||||
hasher.combine(isVar)
|
|
||||||
hasher.combine(offset)
|
|
||||||
hasher.combine(ObjectIdentifier(ownerType))
|
|
||||||
}
|
|
||||||
|
|
||||||
public let name: String
|
public let name: String
|
||||||
public let type: Any.Type
|
public let type: Any.Type
|
||||||
public let isVar: Bool
|
public let isVar: Bool
|
||||||
|
|
|
@ -24,9 +24,7 @@ struct MetadataOffset<Pointee> {
|
||||||
let offset: Int32
|
let offset: Int32
|
||||||
|
|
||||||
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
|
func apply(to ptr: UnsafeRawPointer) -> UnsafePointer<Pointee> {
|
||||||
// Data pointers in constant metadata are absolute until SwiftWasm 5.6
|
#if arch(wasm32)
|
||||||
// Since SwiftWasm 5.7, they are relative as well as other platforms.
|
|
||||||
#if arch(wasm32) && !compiler(>=5.7)
|
|
||||||
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
|
return UnsafePointer<Pointee>(bitPattern: Int(offset))!
|
||||||
#else
|
#else
|
||||||
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)
|
return ptr.advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self)
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
// Copyright 2022 Tokamak contributors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
protocol AnyShapeBox {
|
|
||||||
var animatableDataBox: _AnyAnimatableData { get set }
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path
|
|
||||||
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ConcreteAnyShapeBox<Base: Shape>: AnyShapeBox {
|
|
||||||
var base: Base
|
|
||||||
|
|
||||||
var animatableDataBox: _AnyAnimatableData {
|
|
||||||
get {
|
|
||||||
_AnyAnimatableData(base.animatableData)
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
guard let newData = newValue.value as? Base.AnimatableData else {
|
|
||||||
// TODO: Should this crash?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
base.animatableData = newData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
base.path(in: rect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
|
||||||
base.sizeThatFits(proposal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct AnyShape: Shape {
|
|
||||||
var box: AnyShapeBox
|
|
||||||
|
|
||||||
private init(_ box: AnyShapeBox) {
|
|
||||||
self.box = box
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension AnyShape {
|
|
||||||
init<S: Shape>(_ shape: S) {
|
|
||||||
box = ConcreteAnyShapeBox(base: shape)
|
|
||||||
}
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
box.path(in: rect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
|
||||||
box.sizeThatFits(proposal)
|
|
||||||
}
|
|
||||||
|
|
||||||
var animatableData: _AnyAnimatableData {
|
|
||||||
get { box.animatableDataBox }
|
|
||||||
set { box.animatableDataBox = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,8 +38,7 @@ extension ContainerRelativeShape: InsettableShape {
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
@frozen
|
@frozen internal struct _Inset: InsettableShape, DynamicProperty {
|
||||||
internal struct _Inset: InsettableShape, DynamicProperty {
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
internal var amount: CGFloat
|
internal var amount: CGFloat
|
||||||
@inlinable
|
@inlinable
|
||||||
|
@ -77,8 +76,7 @@ private extension EnvironmentValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
|
||||||
public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
|
|
||||||
public var shape: Shape
|
public var shape: Shape
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(shape: Shape) { self.shape = shape }
|
public init(shape: Shape) { self.shape = shape }
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
|
public struct _StrokedShape<S>: Shape, DynamicProperty where S: Shape {
|
||||||
@Environment(\.self)
|
@Environment(\.self) public var environment
|
||||||
public var environment
|
|
||||||
|
|
||||||
public var shape: S
|
public var shape: S
|
||||||
public var style: StrokeStyle
|
public var style: StrokeStyle
|
||||||
|
|
||||||
|
|
|
@ -89,11 +89,8 @@ extension RoundedRectangle: InsettableShape {
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct _Inset: InsettableShape {
|
struct _Inset: InsettableShape {
|
||||||
@usableFromInline
|
@usableFromInline var base: RoundedRectangle
|
||||||
var base: RoundedRectangle
|
@usableFromInline var amount: CGFloat
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
var amount: CGFloat
|
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
init(base: RoundedRectangle, amount: CGFloat) {
|
init(base: RoundedRectangle, amount: CGFloat) {
|
||||||
|
|
|
@ -21,18 +21,6 @@ public protocol Shape: Animatable, View {
|
||||||
func path(in rect: CGRect) -> Path
|
func path(in rect: CGRect) -> Path
|
||||||
|
|
||||||
static var role: ShapeRole { get }
|
static var role: ShapeRole { get }
|
||||||
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Shape {
|
|
||||||
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
|
|
||||||
// TODO: Check if SwiftUI changes this behavior.
|
|
||||||
|
|
||||||
// SwiftUI seems to not compute the path at all and just return
|
|
||||||
// the following.
|
|
||||||
proposal.replacingUnspecifiedDimensions()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ShapeRole: Hashable {
|
public enum ShapeRole: Hashable {
|
||||||
|
@ -66,15 +54,9 @@ public struct FillStyle: Equatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content: Shape,
|
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
|
||||||
Style: ShapeStyle
|
@Environment(\.self) public var environment
|
||||||
{
|
@Environment(\.foregroundColor) public var foregroundColor
|
||||||
@Environment(\.self)
|
|
||||||
public var environment
|
|
||||||
|
|
||||||
@Environment(\.foregroundColor)
|
|
||||||
public var foregroundColor
|
|
||||||
|
|
||||||
public var shape: Content
|
public var shape: Content
|
||||||
public var style: Style
|
public var style: Style
|
||||||
public var fillStyle: FillStyle
|
public var fillStyle: FillStyle
|
||||||
|
@ -84,32 +66,6 @@ public struct _ShapeView<Content, Style>: _PrimitiveView, Layout where Content:
|
||||||
self.style = style
|
self.style = style
|
||||||
self.fillStyle = fillStyle
|
self.fillStyle = fillStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
public func spacing(subviews: Subviews, cache: inout ()) -> ViewSpacing {
|
|
||||||
.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func sizeThatFits(
|
|
||||||
proposal: ProposedViewSize,
|
|
||||||
subviews: Subviews,
|
|
||||||
cache: inout ()
|
|
||||||
) -> CGSize {
|
|
||||||
proposal.replacingUnspecifiedDimensions()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func placeSubviews(
|
|
||||||
in bounds: CGRect,
|
|
||||||
proposal: ProposedViewSize,
|
|
||||||
subviews: Subviews,
|
|
||||||
cache: inout ()
|
|
||||||
) {
|
|
||||||
for subview in subviews {
|
|
||||||
subview.place(
|
|
||||||
at: bounds.origin,
|
|
||||||
proposal: .init(width: bounds.width, height: bounds.height)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Shape {
|
public extension Shape {
|
||||||
|
|
|
@ -56,8 +56,7 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _BackgroundStyleModifier<Style>: ViewModifier, EnvironmentModifier,
|
||||||
public struct _BackgroundStyleModifier<Style>: ViewModifier, _EnvironmentModifier,
|
|
||||||
EnvironmentReader
|
EnvironmentReader
|
||||||
where Style: ShapeStyle
|
where Style: ShapeStyle
|
||||||
{
|
{
|
||||||
|
|
|
@ -71,10 +71,9 @@ public extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _ForegroundStyleModifier<
|
||||||
public struct _ForegroundStyleModifier<
|
|
||||||
Primary, Secondary, Tertiary
|
Primary, Secondary, Tertiary
|
||||||
>: ViewModifier, _EnvironmentModifier
|
>: ViewModifier, EnvironmentModifier
|
||||||
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
|
where Primary: ShapeStyle, Secondary: ShapeStyle, Tertiary: ShapeStyle
|
||||||
{
|
{
|
||||||
public var primary: Primary
|
public var primary: Primary
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct AngularGradient: ShapeStyle, View {
|
||||||
public struct AngularGradient: ShapeStyle, View {
|
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startAngle: Angle
|
internal var startAngle: Angle
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct EllipticalGradient: ShapeStyle, View {
|
||||||
public struct EllipticalGradient: ShapeStyle, View {
|
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startRadiusFraction: CGFloat
|
internal var startRadiusFraction: CGFloat
|
||||||
|
|
|
@ -17,10 +17,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct Gradient: Equatable {
|
||||||
public struct Gradient: Equatable {
|
@frozen public struct Stop: Equatable {
|
||||||
@frozen
|
|
||||||
public struct Stop: Equatable {
|
|
||||||
public var color: Color
|
public var color: Color
|
||||||
public var location: CGFloat
|
public var location: CGFloat
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct LinearGradient: ShapeStyle, View {
|
||||||
public struct LinearGradient: ShapeStyle, View {
|
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var startPoint: UnitPoint
|
internal var startPoint: UnitPoint
|
||||||
internal var endPoint: UnitPoint
|
internal var endPoint: UnitPoint
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct RadialGradient: ShapeStyle, View {
|
||||||
public struct RadialGradient: ShapeStyle, View {
|
|
||||||
internal var gradient: Gradient
|
internal var gradient: Gradient
|
||||||
internal var center: UnitPoint
|
internal var center: UnitPoint
|
||||||
internal var startRadius: CGFloat
|
internal var startRadius: CGFloat
|
||||||
|
|
|
@ -16,8 +16,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
|
/// A `ShapeStyle` that provides the `primary`, `secondary`, `tertiary`, and `quaternary` styles.
|
||||||
@frozen
|
@frozen public struct HierarchicalShapeStyle: ShapeStyle {
|
||||||
public struct HierarchicalShapeStyle: ShapeStyle {
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
internal var id: UInt32
|
internal var id: UInt32
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
#if canImport(CoreGraphics)
|
|
||||||
import CoreGraphics
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public struct StrokeStyle: Equatable {
|
public struct StrokeStyle: Equatable {
|
||||||
public var lineWidth: CGFloat
|
public var lineWidth: CGFloat
|
||||||
public var lineCap: CGLineCap
|
public var lineCap: CGLineCap
|
||||||
|
|
|
@ -58,12 +58,6 @@ public final class StackReconciler<R: Renderer> {
|
||||||
*/
|
*/
|
||||||
public let rootTarget: R.TargetType
|
public let rootTarget: R.TargetType
|
||||||
|
|
||||||
/** A root renderer's main preference store.
|
|
||||||
*/
|
|
||||||
public var preferenceStore: _PreferenceStore {
|
|
||||||
rootElement.preferenceStore
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A root of the mounted elements tree to which all other mounted elements are attached to.
|
/** A root of the mounted elements tree to which all other mounted elements are attached to.
|
||||||
*/
|
*/
|
||||||
private let rootElement: MountedElement<R>
|
private let rootElement: MountedElement<R>
|
||||||
|
|
|
@ -23,8 +23,7 @@ protocol WritableValueStorage: ValueStorage {
|
||||||
var setter: ((Any, Transaction) -> ())? { get set }
|
var setter: ((Any, Transaction) -> ())? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper public struct State<Value>: DynamicProperty {
|
||||||
public struct State<Value>: DynamicProperty {
|
|
||||||
private let initialValue: Value
|
private let initialValue: Value
|
||||||
|
|
||||||
var anyInitialValue: Any { initialValue }
|
var anyInitialValue: Any { initialValue }
|
||||||
|
|
|
@ -103,7 +103,7 @@ public struct CGAffineTransform: Equatable {
|
||||||
/// Returns an affine transformation matrix constructed from a rotation value you provide.
|
/// Returns an affine transformation matrix constructed from a rotation value you provide.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
|
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
|
||||||
/// A positive value specifies clockwise rotation and a negative value specifies
|
/// A positive value specifies clockwise rotation and anegative value specifies
|
||||||
/// counterclockwise rotation.
|
/// counterclockwise rotation.
|
||||||
public init(rotationAngle angle: CGFloat) {
|
public init(rotationAngle angle: CGFloat) {
|
||||||
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||||
|
|
|
@ -25,8 +25,7 @@
|
||||||
|
|
||||||
public struct ToggleStyleConfiguration {
|
public struct ToggleStyleConfiguration {
|
||||||
public let label: AnyView
|
public let label: AnyView
|
||||||
@Binding
|
@Binding public var isOn: Swift.Bool
|
||||||
public var isOn: Swift.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ToggleStyle {
|
public protocol ToggleStyle {
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// Copyright 2022 Tokamak contributors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
// Created by Carson Katri on 6/20/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
public enum LayoutDirection: Hashable, CaseIterable {
|
|
||||||
case leftToRight
|
|
||||||
case rightToLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EnvironmentValues {
|
|
||||||
private enum LayoutDirectionKey: EnvironmentKey {
|
|
||||||
static var defaultValue: LayoutDirection = .leftToRight
|
|
||||||
}
|
|
||||||
|
|
||||||
public var layoutDirection: LayoutDirection {
|
|
||||||
get { self[LayoutDirectionKey.self] }
|
|
||||||
set { self[LayoutDirectionKey.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,13 +37,11 @@ struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
|
||||||
case tagged(V)
|
case tagged(V)
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable static var defaultValue: Value { .untagged }
|
||||||
static var defaultValue: Value { .untagged }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
|
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
|
||||||
@inlinable
|
@inlinable static var defaultValue: Bool { false }
|
||||||
static var defaultValue: Bool { false }
|
|
||||||
@usableFromInline typealias Value = Bool
|
@usableFromInline typealias Value = Bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@frozen
|
@frozen public struct AnyTransition {
|
||||||
public struct AnyTransition {
|
|
||||||
fileprivate let box: _AnyTransitionBox
|
fileprivate let box: _AnyTransitionBox
|
||||||
|
|
||||||
private init(_ box: _AnyTransitionBox) {
|
private init(_ box: _AnyTransitionBox) {
|
||||||
|
@ -28,16 +27,14 @@ public struct AnyTransition {
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct TransitionTraitKey: _ViewTraitKey {
|
struct TransitionTraitKey: _ViewTraitKey {
|
||||||
@inlinable
|
@inlinable static var defaultValue: AnyTransition { .opacity }
|
||||||
static var defaultValue: AnyTransition { .opacity }
|
|
||||||
|
|
||||||
@usableFromInline typealias Value = AnyTransition
|
@usableFromInline typealias Value = AnyTransition
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
struct CanTransitionTraitKey: _ViewTraitKey {
|
struct CanTransitionTraitKey: _ViewTraitKey {
|
||||||
@inlinable
|
@inlinable static var defaultValue: Bool { false }
|
||||||
static var defaultValue: Bool { false }
|
|
||||||
|
|
||||||
@usableFromInline typealias Value = Bool
|
@usableFromInline typealias Value = Bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,7 @@ public protocol _TraitWritingModifierProtocol {
|
||||||
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@frozen
|
@frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
||||||
public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
|
||||||
where Trait: _ViewTraitKey
|
where Trait: _ViewTraitKey
|
||||||
{
|
{
|
||||||
public let value: Trait.Value
|
public let value: Trait.Value
|
||||||
|
@ -41,14 +40,6 @@ public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierP
|
||||||
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
||||||
viewTraitStore.insert(value, forKey: Trait.self)
|
viewTraitStore.insert(value, forKey: Trait.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func _makeView(_ inputs: ViewInputs<_TraitWritingModifier<Trait>>)
|
|
||||||
-> ViewOutputs
|
|
||||||
{
|
|
||||||
var store = inputs.traits ?? .init()
|
|
||||||
store.insert(inputs.content.value, forKey: Trait.self)
|
|
||||||
return .init(inputs: inputs, traits: store)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ModifiedContent: _TraitWritingModifierProtocol
|
extension ModifiedContent: _TraitWritingModifierProtocol
|
||||||
|
|
|
@ -16,21 +16,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
public struct _ViewTraitStore {
|
public struct _ViewTraitStore {
|
||||||
public var values = [ObjectIdentifier: Any]()
|
public var values = [String: Any]()
|
||||||
|
|
||||||
public init(values: [ObjectIdentifier: Any] = [:]) {
|
public init(values: [String: Any] = [:]) {
|
||||||
self.values = values
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
||||||
where Key: _ViewTraitKey
|
where Key: _ViewTraitKey
|
||||||
{
|
{
|
||||||
values[ObjectIdentifier(key)] as? Key.Value ?? Key.defaultValue
|
values[String(reflecting: key)] as? Key.Value ?? Key.defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||||
where Key: _ViewTraitKey
|
where Key: _ViewTraitKey
|
||||||
{
|
{
|
||||||
values[ObjectIdentifier(key)] = value
|
values[String(reflecting: key)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,6 @@ public struct AnyView: _PrimitiveView {
|
||||||
*/
|
*/
|
||||||
let bodyType: Any.Type
|
let bodyType: Any.Type
|
||||||
|
|
||||||
let visitChildren: (ViewVisitor, Any) -> ()
|
|
||||||
|
|
||||||
public init<V>(_ view: V) where V: View {
|
public init<V>(_ view: V) where V: View {
|
||||||
if let anyView = view as? AnyView {
|
if let anyView = view as? AnyView {
|
||||||
self = anyView
|
self = anyView
|
||||||
|
@ -54,14 +52,8 @@ public struct AnyView: _PrimitiveView {
|
||||||
self.view = view
|
self.view = view
|
||||||
// swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
bodyClosure = { AnyView(($0 as! V).body) }
|
bodyClosure = { AnyView(($0 as! V).body) }
|
||||||
// swiftlint:disable:next force_cast
|
|
||||||
visitChildren = { $0.visit($1 as! V) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func _visitChildren<V>(_ visitor: V) where V: ViewVisitor {
|
|
||||||
visitChildren(visitor, view)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue