Add View Traits and transitions (#426)
This commit is contained in:
parent
ae0db4d1f1
commit
9a568ab9cf
|
@ -9,6 +9,8 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
207C05702610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; };
|
||||
207C05712610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; };
|
||||
26136823269E8EB5006F372E /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26136822269E8EB5006F372E /* TransitionDemo.swift */; };
|
||||
26136824269E8EB5006F372E /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26136822269E8EB5006F372E /* TransitionDemo.swift */; };
|
||||
262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
|
||||
262DA7B42695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
|
||||
26AC04B62698D33A0057784E /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC04B52698D33A0057784E /* ProgressViewDemo.swift */; };
|
||||
|
@ -103,6 +105,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
|
||||
26136822269E8EB5006F372E /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
|
||||
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
|
||||
26AC04B52698D33A0057784E /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
|
||||
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
|
||||
|
@ -200,6 +203,7 @@
|
|||
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
26136822269E8EB5006F372E /* TransitionDemo.swift */,
|
||||
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */,
|
||||
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */,
|
||||
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
|
||||
|
@ -379,6 +383,7 @@
|
|||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
||||
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
|
||||
26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */,
|
||||
26136823269E8EB5006F372E /* TransitionDemo.swift in Sources */,
|
||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
|
@ -415,6 +420,7 @@
|
|||
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
|
||||
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
|
||||
26A3BFB1269BD18A0004DA16 /* AnimationDemo.swift in Sources */,
|
||||
26136824269E8EB5006F372E /* TransitionDemo.swift in Sources */,
|
||||
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
|
||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
|
|
|
@ -177,7 +177,13 @@ let package = Package(
|
|||
condition: .when(platforms: [.wasi])
|
||||
),
|
||||
],
|
||||
resources: [.copy("logo-header.png")]
|
||||
resources: [.copy("logo-header.png")],
|
||||
linkerSettings: [
|
||||
.unsafeFlags(
|
||||
["-Xlinker", "--stack-first", "-Xlinker", "-z", "-Xlinker", "stack-size=16777216"],
|
||||
.when(platforms: [.wasi])
|
||||
),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakStaticHTMLDemo",
|
||||
|
|
|
@ -26,7 +26,7 @@ public struct Transaction {
|
|||
|
||||
public init(animation: Animation?) {
|
||||
self.animation = animation
|
||||
disablesAnimations = true
|
||||
disablesAnimations = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ extension ModifiedContent: EnvironmentReader where Modifier: EnvironmentReader {
|
|||
}
|
||||
}
|
||||
|
||||
extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier {
|
||||
extension ModifiedContent: View, GroupView, ParentView where Content: View, Modifier: ViewModifier {
|
||||
public var body: Body {
|
||||
neverBody("ModifiedContent<View, ViewModifier>")
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ public protocol ViewModifier {
|
|||
func body(content: Content) -> Self.Body
|
||||
}
|
||||
|
||||
public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier {
|
||||
public struct _ViewModifier_Content<Modifier>: View
|
||||
where Modifier: ViewModifier
|
||||
{
|
||||
public let modifier: Modifier
|
||||
public let view: AnyView
|
||||
|
||||
|
@ -27,7 +29,7 @@ public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier
|
|||
self.view = view
|
||||
}
|
||||
|
||||
public var body: AnyView {
|
||||
public var body: some View {
|
||||
view
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,21 +22,33 @@ import OpenCombineShim
|
|||
// `View`s
|
||||
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
||||
override func mount(
|
||||
before _: R.TargetType? = nil,
|
||||
on _: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
// `App` elements have no siblings, hence the `before` argument is discarded.
|
||||
// They also have no parents, so the `parent` argument is discarded as well.
|
||||
let childBody = reconciler.render(mountedApp: self)
|
||||
|
||||
let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: nil, on: self, with: reconciler)
|
||||
child.transaction = transaction
|
||||
child.mount(before: nil, on: self, in: reconciler, with: transaction)
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
mountedChildren
|
||||
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||
}
|
||||
|
||||
/// Mounts a child scene within the app.
|
||||
|
|
|
@ -61,10 +61,11 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
|||
_ view: AnyView,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
self.parentTarget = parentTarget
|
||||
super.init(view, environmentValues, parent)
|
||||
super.init(view, environmentValues, viewTraits, parent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,20 +21,32 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
var transaction = transaction
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
// Disable animations on mount so `animation(_:)` doesn't try to animate
|
||||
// until the transition finishes.
|
||||
transaction.disablesAnimations = true
|
||||
self.transaction = transaction
|
||||
|
||||
let childBody = reconciler.render(compositeView: self)
|
||||
|
||||
if let traitModifier = view.view as? _TraitWritingModifierProtocol {
|
||||
traitModifier.modifyViewTraitStore(&viewTraits)
|
||||
}
|
||||
let child: MountedElement<R> = childBody.makeMountedView(
|
||||
reconciler.renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
viewTraits,
|
||||
self
|
||||
)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: sibling, on: self, with: reconciler)
|
||||
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||
|
||||
// `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite
|
||||
// view, so it's enough check for it only here.
|
||||
|
@ -73,10 +85,25 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
preferenceReader.preferenceStore(self.preferenceStore)
|
||||
}
|
||||
})
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
|
||||
var transaction = transaction
|
||||
transaction.disablesAnimations = false
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
|
||||
mountedChildren.forEach {
|
||||
$0.viewTraits = self.viewTraits
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
}
|
||||
|
||||
if let appearanceAction = view.view as? AppearanceActionType {
|
||||
appearanceAction.disappear?()
|
||||
|
@ -85,6 +112,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
var transaction = transaction
|
||||
transaction.disablesAnimations = false
|
||||
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
|
||||
let element = reconciler.render(compositeView: self)
|
||||
reconciler.reconcile(
|
||||
|
@ -98,7 +126,13 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
|||
$0.transaction = transaction
|
||||
},
|
||||
mountChild: {
|
||||
$0.makeMountedView(reconciler.renderer, parentTarget, environmentValues, self)
|
||||
$0.makeMountedView(
|
||||
reconciler.renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
viewTraits,
|
||||
self
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -86,11 +86,15 @@ public class MountedElement<R: Renderer> {
|
|||
|
||||
var mountedChildren = [MountedElement<R>]()
|
||||
|
||||
public internal(set) var transaction: Transaction = .init(animation: nil)
|
||||
public var transaction: Transaction = .init(animation: nil)
|
||||
/// Where this element is the process of mounting/unmounting.
|
||||
var transitionPhase = TransitionPhase.willMount
|
||||
/// The current `UnmountTask` of this element.
|
||||
var unmountTask: UnmountTask<R>?
|
||||
|
||||
var environmentValues: EnvironmentValues
|
||||
public internal(set) var environmentValues: EnvironmentValues
|
||||
|
||||
unowned 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() {
|
||||
didSet {
|
||||
|
@ -98,10 +102,13 @@ public class MountedElement<R: Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
public internal(set) var viewTraits: _ViewTraitStore
|
||||
|
||||
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
element = .app(app)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
}
|
||||
|
||||
|
@ -109,13 +116,20 @@ public class MountedElement<R: Renderer> {
|
|||
element = .scene(scene)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
viewTraits = .init()
|
||||
updateEnvironment()
|
||||
}
|
||||
|
||||
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||
init(
|
||||
_ view: AnyView,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
element = .view(view)
|
||||
self.parent = parent
|
||||
self.environmentValues = environmentValues
|
||||
self.viewTraits = viewTraits
|
||||
updateEnvironment()
|
||||
}
|
||||
|
||||
|
@ -131,16 +145,68 @@ public class MountedElement<R: Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
/// You must call `super.prepareForMount` before all other mounting work.
|
||||
func prepareForMount(with transaction: Transaction) {
|
||||
// `GroupView`'s don't really mount, so let their children transition if the group can.
|
||||
if case let .view(view) = element,
|
||||
view.type is GroupView.Type
|
||||
{
|
||||
transitionPhase = parent?.transitionPhase ?? .normal
|
||||
}
|
||||
// Allow the root of a mount to transition
|
||||
// (if their parent isn't mounting, then they are the root of the mount).
|
||||
if parent?.transitionPhase == .normal {
|
||||
viewTraits.insert(
|
||||
transaction.animation != nil
|
||||
|| _AnyTransitionProxy(viewTraits.transition)
|
||||
.resolve(in: environmentValues)
|
||||
.insertionAnimation != nil,
|
||||
forKey: CanTransitionTraitKey.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// You must call `super.mount` after all other mounting work.
|
||||
func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
// Set the phase to `normal` after finished mounting.
|
||||
transitionPhase = .normal
|
||||
}
|
||||
|
||||
func unmount(with reconciler: StackReconciler<R>) {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
/// You must call `super.unmount` before all other unmounting work.
|
||||
func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
if !(self is MountedHostView<R>) {
|
||||
unmountTask = parentTask?.appendChild()
|
||||
}
|
||||
|
||||
// `GroupView`'s don't really unmount, so let their children transition if the group can.
|
||||
if case let .view(view) = element,
|
||||
view.type is GroupView.Type
|
||||
{
|
||||
transitionPhase = parent?.transitionPhase ?? .normal
|
||||
} else {
|
||||
// Set the phase to `willUnmount` before unmounting.
|
||||
transitionPhase = .willUnmount
|
||||
}
|
||||
// Allow the root of an unmount to transition
|
||||
// (if their parent isn't unmounting, then they are the root of the unmount).
|
||||
if parent?.transitionPhase == .normal {
|
||||
viewTraits.insert(
|
||||
transaction.animation != nil
|
||||
|| _AnyTransitionProxy(viewTraits.transition)
|
||||
.resolve(in: environmentValues)
|
||||
.removalAnimation != nil,
|
||||
forKey: CanTransitionTraitKey.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
|
@ -228,14 +294,15 @@ extension AnyView {
|
|||
_ renderer: R,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) -> MountedElement<R> {
|
||||
if type == EmptyView.self {
|
||||
return MountedEmptyView(self, environmentValues, parent)
|
||||
return MountedEmptyView(self, environmentValues, viewTraits, parent)
|
||||
} else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
|
||||
return MountedHostView(self, parentTarget, environmentValues, parent)
|
||||
return MountedHostView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||
} else {
|
||||
return MountedCompositeView(self, parentTarget, environmentValues, parent)
|
||||
return MountedCompositeView(self, parentTarget, environmentValues, viewTraits, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,20 @@ final class MountedEmptyView<R: Renderer>: MountedElement<R> {
|
|||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
) {}
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {}
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
}
|
||||
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction?) {}
|
||||
}
|
||||
|
|
|
@ -33,18 +33,24 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
_ view: AnyView,
|
||||
_ parentTarget: R.TargetType,
|
||||
_ environmentValues: EnvironmentValues,
|
||||
_ viewTraits: _ViewTraitStore,
|
||||
_ parent: MountedElement<R>?
|
||||
) {
|
||||
self.parentTarget = parentTarget
|
||||
|
||||
super.init(view, environmentValues, parent)
|
||||
super.init(view, environmentValues, viewTraits, parent)
|
||||
}
|
||||
|
||||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
|
||||
self.transaction = transaction
|
||||
|
||||
guard let target = reconciler.renderer.mountTarget(
|
||||
before: sibling,
|
||||
to: parentTarget,
|
||||
|
@ -56,8 +62,17 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
|
||||
guard !view.children.isEmpty else { return }
|
||||
|
||||
let isGroupView = view.type is GroupView.Type
|
||||
// Don't allow children to transition their mounting since they aren't individually
|
||||
// appearing (unless its a `GroupView`, which is flattened).
|
||||
mountedChildren = view.children.map {
|
||||
$0.makeMountedView(reconciler.renderer, target, environmentValues, self)
|
||||
$0.makeMountedView(
|
||||
reconciler.renderer,
|
||||
target,
|
||||
environmentValues,
|
||||
isGroupView ? self.viewTraits : .init(),
|
||||
self
|
||||
)
|
||||
}
|
||||
|
||||
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
|
||||
|
@ -65,46 +80,73 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
|
||||
`GroupView`.
|
||||
*/
|
||||
let isGroupView = view.type is GroupView.Type
|
||||
mountedChildren.forEach {
|
||||
$0.mount(before: isGroupView ? sibling : nil, on: self, with: reconciler)
|
||||
$0.mount(before: isGroupView ? sibling : nil, on: self, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
private var parentUnmountTask = UnmountTask<R>()
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
|
||||
guard let target = target else { return }
|
||||
|
||||
let task = UnmountHostTask(self, in: reconciler) {
|
||||
self.mountedChildren.forEach {
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: self.unmountTask)
|
||||
}
|
||||
}
|
||||
task.isCancelled = parentTask?.isCancelled ?? false
|
||||
unmountTask = task
|
||||
parentTask?.childTasks.append(task)
|
||||
reconciler.renderer.unmount(
|
||||
target: target,
|
||||
from: parentTarget,
|
||||
with: self
|
||||
) {
|
||||
self.mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
}
|
||||
with: task
|
||||
)
|
||||
}
|
||||
|
||||
/// Stop any unfinished unmounts and complete them without transitions.
|
||||
private func invalidateUnmount() {
|
||||
parentUnmountTask.cancel()
|
||||
parentUnmountTask.completeImmediately()
|
||||
parentUnmountTask = .init()
|
||||
}
|
||||
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
guard let target = target else { return }
|
||||
|
||||
invalidateUnmount()
|
||||
|
||||
updateEnvironment()
|
||||
target.view = view
|
||||
reconciler.renderer.update(target: target, with: self)
|
||||
|
||||
var childrenViews = view.children
|
||||
|
||||
let traits = view.type is GroupView.Type ? viewTraits : .init()
|
||||
|
||||
switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
|
||||
// if existing children present and new children array is empty
|
||||
// then unmount all existing children
|
||||
case (false, true):
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
mountedChildren.forEach {
|
||||
$0.unmount(in: reconciler, with: transaction, parentTask: self.parentUnmountTask)
|
||||
}
|
||||
mountedChildren = []
|
||||
|
||||
// if no existing children then mount all new children
|
||||
case (true, false):
|
||||
mountedChildren = childrenViews.map {
|
||||
$0.makeMountedView(reconciler.renderer, target, environmentValues, self)
|
||||
$0.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||
}
|
||||
mountedChildren.forEach { $0.mount(on: self, with: reconciler) }
|
||||
mountedChildren.forEach { $0.mount(on: self, in: reconciler, with: transaction) }
|
||||
|
||||
// if both arrays have items then reconcile by types and keys
|
||||
case (false, false):
|
||||
|
@ -130,10 +172,13 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
reconciler.renderer,
|
||||
target,
|
||||
environmentValues,
|
||||
traits,
|
||||
self
|
||||
)
|
||||
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler)
|
||||
mountedChild.unmount(with: reconciler)
|
||||
newChild.mount(
|
||||
before: mountedChild.firstDescendantTarget, on: self, in: reconciler, with: transaction
|
||||
)
|
||||
mountedChild.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||
}
|
||||
newChildren.append(newChild)
|
||||
mountedChildren.removeFirst()
|
||||
|
@ -144,15 +189,15 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
|||
// unmount remaining `mountedChildren`
|
||||
if !mountedChildren.isEmpty {
|
||||
for child in mountedChildren {
|
||||
child.unmount(with: reconciler)
|
||||
child.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
|
||||
}
|
||||
} else {
|
||||
// more views left than children were mounted,
|
||||
// mount remaining views
|
||||
for firstChild in childrenViews {
|
||||
let newChild: MountedElement<R> =
|
||||
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, self)
|
||||
newChild.mount(on: self, with: reconciler)
|
||||
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
|
||||
newChild.mount(on: self, in: reconciler, with: transaction)
|
||||
newChildren.append(newChild)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,18 +31,28 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
|||
override func mount(
|
||||
before sibling: R.TargetType? = nil,
|
||||
on parent: MountedElement<R>? = nil,
|
||||
with reconciler: StackReconciler<R>
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction
|
||||
) {
|
||||
super.prepareForMount(with: transaction)
|
||||
let childBody = reconciler.render(mountedScene: self)
|
||||
|
||||
let child: MountedElement<R> = childBody
|
||||
.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
|
||||
mountedChildren = [child]
|
||||
child.mount(before: sibling, on: self, with: reconciler)
|
||||
child.mount(before: sibling, on: self, in: reconciler, with: transaction)
|
||||
|
||||
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
|
||||
}
|
||||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
override func unmount(
|
||||
in reconciler: StackReconciler<R>,
|
||||
with transaction: Transaction,
|
||||
parentTask: UnmountTask<R>?
|
||||
) {
|
||||
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
|
||||
mountedChildren
|
||||
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
|
||||
}
|
||||
|
||||
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
|
||||
|
@ -89,7 +99,7 @@ extension _AnyScene.BodyResult {
|
|||
case let .scene(scene):
|
||||
return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent)
|
||||
case let .view(view):
|
||||
return view.makeMountedView(renderer, parentTarget, environmentValues, parent)
|
||||
return view.makeMountedView(renderer, parentTarget, environmentValues, .init(), parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +124,7 @@ extension _AnyScene {
|
|||
renderer,
|
||||
parentTarget,
|
||||
environmentValues,
|
||||
.init(),
|
||||
parent
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2018-2021 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/19/21.
|
||||
//
|
||||
|
||||
/// A tree of cancellable in-progress unmounts.
|
||||
public class UnmountTask<R> where R: Renderer {
|
||||
public internal(set) var isCancelled = false
|
||||
var childTasks = [UnmountTask<R>]()
|
||||
private let callback: () -> ()
|
||||
|
||||
init(_ callback: @escaping () -> () = {}) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
forEach { $0.isCancelled = true }
|
||||
}
|
||||
|
||||
/// Call after completely unmounting the `host`.
|
||||
public func finish() {
|
||||
callback()
|
||||
}
|
||||
|
||||
/// Adds and returns a new child `UnmountTask`
|
||||
func appendChild() -> UnmountTask<R> {
|
||||
let child = UnmountTask()
|
||||
child.isCancelled = isCancelled
|
||||
childTasks.append(child)
|
||||
return child
|
||||
}
|
||||
|
||||
/// Forces the element and all child tasks to unmount without transition.
|
||||
func completeImmediately() {
|
||||
forEach {
|
||||
guard $0 is UnmountHostTask<R> else { return }
|
||||
$0.completeImmediately()
|
||||
}
|
||||
}
|
||||
|
||||
func forEach(_ f: (UnmountTask<R>) -> ()) {
|
||||
var stack = [self]
|
||||
while let last = stack.popLast() {
|
||||
f(last)
|
||||
stack.insert(contentsOf: last.childTasks, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state for the unmounting of a `MountedHostView` by a `Renderer`.
|
||||
public final class UnmountHostTask<R>: UnmountTask<R> where R: Renderer {
|
||||
public private(set) weak var host: MountedHostView<R>!
|
||||
private unowned var reconciler: StackReconciler<R>
|
||||
|
||||
init(
|
||||
_ host: MountedHostView<R>,
|
||||
in reconciler: StackReconciler<R>,
|
||||
callback: @escaping () -> ()
|
||||
) {
|
||||
self.host = host
|
||||
self.reconciler = reconciler
|
||||
super.init(callback)
|
||||
}
|
||||
|
||||
override func completeImmediately() {
|
||||
host.viewTraits.insert(false, forKey: CanTransitionTraitKey.self)
|
||||
host.unmount(in: reconciler, with: .init(animation: nil), parentTask: nil)
|
||||
}
|
||||
}
|
|
@ -58,13 +58,12 @@ public protocol Renderer: AnyObject {
|
|||
unmounted: removed from the parent and most likely destroyed.
|
||||
- parameter target: Existing target instance to be unmounted.
|
||||
- parameter parent: Parent of target to direct interaction with parent.
|
||||
- parameter view: The host view that renders to the updated target.
|
||||
- parameter task: The state associated with the unmount.
|
||||
*/
|
||||
func unmount(
|
||||
target: TargetType,
|
||||
from parent: TargetType,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
with task: UnmountHostTask<Self>
|
||||
)
|
||||
|
||||
/** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for
|
||||
|
|
|
@ -86,7 +86,7 @@ public final class StackReconciler<R: Renderer> {
|
|||
self.scheduler = scheduler
|
||||
rootTarget = target
|
||||
|
||||
rootElement = AnyView(view).makeMountedView(renderer, target, environment, nil)
|
||||
rootElement = AnyView(view).makeMountedView(renderer, target, environment, .init(), nil)
|
||||
|
||||
performInitialMount()
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ public final class StackReconciler<R: Renderer> {
|
|||
}
|
||||
|
||||
private func performInitialMount() {
|
||||
rootElement.mount(with: self)
|
||||
rootElement.mount(in: self, with: .init(animation: nil))
|
||||
performPostrenderCallbacks()
|
||||
}
|
||||
|
||||
|
@ -287,7 +287,7 @@ public final class StackReconciler<R: Renderer> {
|
|||
case let (nil, childBody):
|
||||
let child: MountedElement<R> = mountChild(childBody)
|
||||
mountedElement.mountedChildren = [child]
|
||||
child.mount(with: self)
|
||||
child.mount(in: self, with: transaction)
|
||||
|
||||
// some mounted children before and now
|
||||
case let (mountedChild?, childBody):
|
||||
|
@ -300,11 +300,11 @@ public final class StackReconciler<R: Renderer> {
|
|||
} else {
|
||||
// new child is of a different type, complete rerender, i.e. unmount the old
|
||||
// wrapper, then mount a new one with the new `childBody`
|
||||
mountedChild.unmount(with: self)
|
||||
mountedChild.unmount(in: self, with: transaction, parentTask: nil)
|
||||
|
||||
let newMountedChild: MountedElement<R> = mountChild(childBody)
|
||||
mountedElement.mountedChildren = [newMountedChild]
|
||||
newMountedChild.mount(with: self)
|
||||
newMountedChild.mount(in: self, with: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@frozen public struct AnyTransition {
|
||||
fileprivate let box: _AnyTransitionBox
|
||||
|
||||
private init(_ box: _AnyTransitionBox) {
|
||||
self.box = box
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct TransitionTraitKey: _ViewTraitKey {
|
||||
@inlinable static var defaultValue: AnyTransition { .opacity }
|
||||
|
||||
@usableFromInline typealias Value = AnyTransition
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct CanTransitionTraitKey: _ViewTraitKey {
|
||||
@inlinable static var defaultValue: Bool { false }
|
||||
|
||||
@usableFromInline typealias Value = Bool
|
||||
}
|
||||
|
||||
public extension _ViewTraitStore {
|
||||
var transition: AnyTransition { value(forKey: TransitionTraitKey.self) }
|
||||
var canTransition: Bool { value(forKey: CanTransitionTraitKey.self) }
|
||||
}
|
||||
|
||||
enum TransitionPhase: Hashable {
|
||||
case willMount
|
||||
case normal
|
||||
case willUnmount
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func transition(_ t: AnyTransition) -> some View {
|
||||
_trait(TransitionTraitKey.self, t)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ViewModifier` used to apply a primitive transition to a `View`.
|
||||
public protocol _AnyTransitionModifier: AnimatableModifier
|
||||
where Body == Self.Content
|
||||
{
|
||||
var isActive: Bool { get }
|
||||
}
|
||||
|
||||
public extension _AnyTransitionModifier {
|
||||
func body(content: Content) -> Body {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
public struct _MoveTransition: _AnyTransitionModifier {
|
||||
public let edge: Edge
|
||||
public let isActive: Bool
|
||||
public typealias Body = Self.Content
|
||||
}
|
||||
|
||||
public extension AnyTransition {
|
||||
static let identity: AnyTransition = .init(IdentityTransitionBox())
|
||||
|
||||
static func move(edge: Edge) -> AnyTransition {
|
||||
modifier(
|
||||
active: _MoveTransition(edge: edge, isActive: true),
|
||||
identity: _MoveTransition(edge: edge, isActive: false)
|
||||
)
|
||||
}
|
||||
|
||||
static func asymmetric(
|
||||
insertion: AnyTransition,
|
||||
removal: AnyTransition
|
||||
) -> AnyTransition {
|
||||
.init(AsymmetricTransitionBox(insertion: insertion.box, removal: removal.box))
|
||||
}
|
||||
|
||||
static func offset(_ offset: CGSize) -> AnyTransition {
|
||||
modifier(
|
||||
active: _OffsetEffect(offset: offset),
|
||||
identity: _OffsetEffect(offset: .zero)
|
||||
)
|
||||
}
|
||||
|
||||
static func offset(
|
||||
x: CGFloat = 0,
|
||||
y: CGFloat = 0
|
||||
) -> AnyTransition {
|
||||
offset(.init(width: x, height: y))
|
||||
}
|
||||
|
||||
static var scale: AnyTransition { scale(scale: 0) }
|
||||
static func scale(scale: CGFloat, anchor: UnitPoint = .center) -> AnyTransition {
|
||||
modifier(
|
||||
active: _ScaleEffect(scale: .init(width: scale, height: scale), anchor: anchor),
|
||||
identity: _ScaleEffect(scale: .init(width: 1, height: 1), anchor: anchor)
|
||||
)
|
||||
}
|
||||
|
||||
static let opacity: AnyTransition = modifier(
|
||||
active: _OpacityEffect(opacity: 0),
|
||||
identity: _OpacityEffect(opacity: 1)
|
||||
)
|
||||
|
||||
static let slide: AnyTransition = asymmetric(
|
||||
insertion: .move(edge: .leading),
|
||||
removal: .move(edge: .trailing)
|
||||
)
|
||||
|
||||
static func modifier<E>(
|
||||
active: E,
|
||||
identity: E
|
||||
) -> AnyTransition where E: ViewModifier {
|
||||
.init(
|
||||
ConcreteTransitionBox(
|
||||
(active: {
|
||||
AnyView($0.modifier(active))
|
||||
}, identity: {
|
||||
AnyView($0.modifier(identity))
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func combined(with other: AnyTransition) -> AnyTransition {
|
||||
.init(CombinedTransitionBox(a: box, b: other.box))
|
||||
}
|
||||
|
||||
func animation(_ animation: Animation?) -> AnyTransition {
|
||||
.init(AnimatedTransitionBox(animation: animation, parent: box))
|
||||
}
|
||||
}
|
||||
|
||||
public struct _AnyTransitionProxy {
|
||||
let subject: AnyTransition
|
||||
|
||||
public init(_ subject: AnyTransition) { self.subject = subject }
|
||||
|
||||
public func resolve(
|
||||
in environment: EnvironmentValues
|
||||
) -> _AnyTransitionBox.ResolvedValue {
|
||||
subject.box.resolve(in: environment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class _AnyTransitionBox: AnyTokenBox {
|
||||
public typealias ResolvedValue = ResolvedTransition
|
||||
|
||||
public struct ResolvedTransition {
|
||||
public var insertion: [Transition]
|
||||
public var removal: [Transition]
|
||||
public var insertionAnimation: Animation?
|
||||
public var removalAnimation: Animation?
|
||||
|
||||
init(
|
||||
insertion: [Transition],
|
||||
removal: [Transition],
|
||||
insertionAnimation: Animation?,
|
||||
removalAnimation: Animation?
|
||||
) {
|
||||
self.insertion = insertion
|
||||
self.removal = removal
|
||||
self.insertionAnimation = insertionAnimation
|
||||
self.removalAnimation = removalAnimation
|
||||
}
|
||||
|
||||
init(transitions: [Transition]) {
|
||||
self.init(
|
||||
insertion: transitions,
|
||||
removal: transitions,
|
||||
insertionAnimation: nil,
|
||||
removalAnimation: nil
|
||||
)
|
||||
}
|
||||
|
||||
public typealias Transition = (
|
||||
active: (AnyView) -> AnyView,
|
||||
identity: (AnyView) -> AnyView
|
||||
)
|
||||
}
|
||||
|
||||
public func resolve(in environment: EnvironmentValues) -> ResolvedValue {
|
||||
fatalError("implement \(#function) in subclass")
|
||||
}
|
||||
}
|
||||
|
||||
final class IdentityTransitionBox: _AnyTransitionBox {
|
||||
override func resolve(in environment: EnvironmentValues) -> _AnyTransitionBox.ResolvedValue {
|
||||
.init(transitions: [])
|
||||
}
|
||||
}
|
||||
|
||||
final class ConcreteTransitionBox: _AnyTransitionBox {
|
||||
let transition: ResolvedTransition.Transition
|
||||
|
||||
init(_ transition: ResolvedTransition.Transition) {
|
||||
self.transition = transition
|
||||
}
|
||||
|
||||
override func resolve(in environment: EnvironmentValues) -> _AnyTransitionBox.ResolvedValue {
|
||||
.init(transitions: [transition])
|
||||
}
|
||||
}
|
||||
|
||||
final class AsymmetricTransitionBox: _AnyTransitionBox {
|
||||
let insertion: _AnyTransitionBox
|
||||
let removal: _AnyTransitionBox
|
||||
|
||||
init(insertion: _AnyTransitionBox, removal: _AnyTransitionBox) {
|
||||
self.insertion = insertion
|
||||
self.removal = removal
|
||||
}
|
||||
|
||||
override func resolve(in environment: EnvironmentValues) -> ResolvedValue {
|
||||
let insertionResolved = insertion.resolve(in: environment)
|
||||
let removalResolved = removal.resolve(in: environment)
|
||||
return .init(
|
||||
insertion: insertionResolved.insertion,
|
||||
removal: removalResolved.removal,
|
||||
insertionAnimation: insertionResolved.insertionAnimation,
|
||||
removalAnimation: removalResolved.removalAnimation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class CombinedTransitionBox: _AnyTransitionBox {
|
||||
let a: _AnyTransitionBox
|
||||
let b: _AnyTransitionBox
|
||||
|
||||
init(a: _AnyTransitionBox, b: _AnyTransitionBox) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
}
|
||||
|
||||
override func resolve(in environment: EnvironmentValues) -> ResolvedValue {
|
||||
let aResolved = a.resolve(in: environment)
|
||||
let bResolved = b.resolve(in: environment)
|
||||
return .init(
|
||||
insertion: aResolved.insertion + bResolved.insertion,
|
||||
removal: aResolved.removal + bResolved.removal,
|
||||
insertionAnimation: bResolved.insertionAnimation ?? aResolved.insertionAnimation,
|
||||
removalAnimation: bResolved.removalAnimation ?? aResolved.removalAnimation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class AnimatedTransitionBox: _AnyTransitionBox {
|
||||
let animation: Animation?
|
||||
let parent: _AnyTransitionBox
|
||||
|
||||
init(animation: Animation?, parent: _AnyTransitionBox) {
|
||||
self.animation = animation
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
override func resolve(in environment: EnvironmentValues) -> ResolvedValue {
|
||||
var resolved = parent.resolve(in: environment)
|
||||
resolved.insertionAnimation = animation
|
||||
resolved.removalAnimation = animation
|
||||
return resolved
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/10/21.
|
||||
//
|
||||
|
||||
public protocol _ViewTraitKey {
|
||||
associatedtype Value
|
||||
static var defaultValue: Value { get }
|
||||
}
|
||||
|
||||
public protocol _TraitWritingModifierProtocol {
|
||||
func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore)
|
||||
}
|
||||
|
||||
@frozen public struct _TraitWritingModifier<Trait>: ViewModifier, _TraitWritingModifierProtocol
|
||||
where Trait: _ViewTraitKey
|
||||
{
|
||||
public let value: Trait.Value
|
||||
@inlinable
|
||||
public init(value: Trait.Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
}
|
||||
|
||||
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
||||
viewTraitStore.insert(value, forKey: Trait.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ModifiedContent: _TraitWritingModifierProtocol
|
||||
where Modifier: _TraitWritingModifierProtocol
|
||||
{
|
||||
public func modifyViewTraitStore(_ viewTraitStore: inout _ViewTraitStore) {
|
||||
modifier.modifyViewTraitStore(&viewTraitStore)
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
@inlinable
|
||||
func _trait<K>(
|
||||
_ key: K.Type,
|
||||
_ value: K.Value
|
||||
) -> some View where K: _ViewTraitKey {
|
||||
modifier(_TraitWritingModifier<K>(value: value))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/10/21.
|
||||
//
|
||||
|
||||
public struct _ViewTraitStore {
|
||||
public var values = [String: Any]()
|
||||
|
||||
public init(values: [String: Any] = [:]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
public func value<Key>(forKey key: Key.Type = Key.self) -> Key.Value
|
||||
where Key: _ViewTraitKey
|
||||
{
|
||||
values[String(reflecting: key)] as? Key.Value ?? Key.defaultValue
|
||||
}
|
||||
|
||||
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||
where Key: _ViewTraitKey
|
||||
{
|
||||
values[String(reflecting: key)] = value
|
||||
}
|
||||
}
|
|
@ -189,6 +189,8 @@ public typealias EmptyAnimatableData = TokamakCore.EmptyAnimatableData
|
|||
|
||||
public typealias AnimatableModifier = TokamakCore.AnimatableModifier
|
||||
|
||||
public typealias AnyTransition = TokamakCore.AnyTransition
|
||||
|
||||
public func withTransaction<Result>(
|
||||
_ transaction: Transaction,
|
||||
_ body: () throws -> Result
|
||||
|
|
|
@ -54,119 +54,24 @@ extension _AnimationBoxBase._Resolved._RepeatStyle {
|
|||
}
|
||||
|
||||
extension AnyHTML {
|
||||
func update(dom: DOMNode, transaction: Transaction) {
|
||||
// FIXME: is there a sensible way to diff attributes and listeners to avoid
|
||||
// crossing the JavaScript bridge and touching DOM if not needed?
|
||||
func update(
|
||||
dom: DOMNode,
|
||||
computeStart: Bool = true,
|
||||
additionalAttributes: [HTMLAttribute: String],
|
||||
transaction: Transaction
|
||||
) {
|
||||
let attributes = self.attributes.merging(additionalAttributes, uniquingKeysWith: +)
|
||||
|
||||
// @carson-katri: For diffing, could you build a Set from the keys and values of the dictionary,
|
||||
// then use the standard lib to get the difference?
|
||||
dom.applyAttributes(attributes, with: transaction)
|
||||
|
||||
// `checked` attribute on checkboxes is a special one as its value doesn't matter. We only
|
||||
// need to check whether it exists or not, and set the property if it doesn't.
|
||||
var containsChecked = false
|
||||
for (attribute, value) in attributes {
|
||||
// Animate styles with the Web Animations API further down.
|
||||
guard transaction.animation == nil || attribute != "style"
|
||||
else { continue }
|
||||
|
||||
if attribute == "style" { // Clear animations
|
||||
dom.ref.getAnimations?().array?.forEach { _ = $0.cancel() }
|
||||
}
|
||||
|
||||
if attribute.isUpdatedAsProperty {
|
||||
dom.ref[dynamicMember: attribute.value] = .string(value)
|
||||
} else {
|
||||
_ = dom.ref.setAttribute!(attribute.value, value)
|
||||
}
|
||||
|
||||
if attribute == .checked {
|
||||
containsChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
// Animate styles
|
||||
if let style = attributes["style"],
|
||||
let animation = transaction.animation
|
||||
if !transaction.disablesAnimations,
|
||||
let animation = transaction.animation,
|
||||
let style = attributes["style"]
|
||||
{
|
||||
let resolved = _AnimationProxy(animation).resolve()
|
||||
func extractStyles(compute: Bool = false) -> [String: String] {
|
||||
var res = [String: String]()
|
||||
let computedStyle = JSObject.global.getComputedStyle?(dom.ref)
|
||||
for i in 0..<Int(dom.ref.style.object?.length.number ?? 0) {
|
||||
guard let key = dom.ref.style.object?[i].string else { continue }
|
||||
if compute {
|
||||
res[key] = computedStyle?[dynamicMember: key].string
|
||||
?? dom.ref.style.object?[key].string
|
||||
} else {
|
||||
res[key] = dom.ref.style.object?[key].string
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
let startStyle = extractStyles(compute: true).jsValue()
|
||||
dom.ref.style.object?.cssText = .string(style)
|
||||
let endStyle = Dictionary(uniqueKeysWithValues: extractStyles().map {
|
||||
($0.animatableProperty, $1)
|
||||
})
|
||||
|
||||
let duration = (resolved.duration / resolved.speed) * 1000
|
||||
let delay = resolved.delay * 1000
|
||||
|
||||
let keyframes: [JSValue]
|
||||
if case let .solver(solver) = resolved.style {
|
||||
// Compute styles at several intervals.
|
||||
var values = [[String: String]]()
|
||||
for iterationStart in stride(from: 0, to: 1, by: 0.01) {
|
||||
// Create and immediately cancel an animation after reading the computed values.
|
||||
if let animation = dom.ref.animate?(
|
||||
[startStyle, endStyle.jsValue()],
|
||||
[
|
||||
"duration": duration,
|
||||
"delay": delay,
|
||||
"easing": "linear",
|
||||
"iterationStart": iterationStart,
|
||||
]
|
||||
).object,
|
||||
let computedStyle = JSObject.global.getComputedStyle?(dom.ref)
|
||||
{
|
||||
var styles = [String: String]()
|
||||
for key in endStyle.keys {
|
||||
if let string = computedStyle[dynamicMember: key].string {
|
||||
styles[key] = string
|
||||
}
|
||||
}
|
||||
values.append(styles)
|
||||
_ = animation.cancel?()
|
||||
}
|
||||
}
|
||||
// Solve the values
|
||||
keyframes = (0..<values.count).map { t in
|
||||
let offset = Double(t) / Double(values.count - 1)
|
||||
let solved = solver.solve(at: offset * (duration / 1000)) * Double(values.count - 1)
|
||||
var res = values[Int(solved)]
|
||||
res["offset"] = "\(offset)"
|
||||
return res.jsValue()
|
||||
} + [endStyle.jsValue()] // Add the end for good measure.
|
||||
} else {
|
||||
keyframes = [startStyle, endStyle.jsValue()]
|
||||
}
|
||||
// Animate the styles.
|
||||
_ = dom.ref.animate?(
|
||||
keyframes.jsValue(),
|
||||
[
|
||||
"duration": duration,
|
||||
"delay": delay,
|
||||
"easing": resolved.style.cssValue,
|
||||
"iterations": resolved.repeatStyle.jsValue,
|
||||
"direction": resolved.repeatStyle.autoreverses ? "alternate" : "normal",
|
||||
// Keep the last keyframe applied when done, and the first applied during a delay.
|
||||
"fill": "both",
|
||||
]
|
||||
)
|
||||
dom.animateStyles(to: style, computeStart: computeStart, with: animation)
|
||||
}
|
||||
|
||||
if !containsChecked && dom.ref.type == "checkbox" &&
|
||||
if attributes[.checked] == nil && dom.ref.type == "checkbox" &&
|
||||
dom.ref.tagName.string!.lowercased() == "input"
|
||||
{
|
||||
dom.ref.checked = .boolean(false)
|
||||
|
@ -217,4 +122,123 @@ final class DOMNode: Target {
|
|||
self.listeners[event] = jsClosure
|
||||
}
|
||||
}
|
||||
|
||||
func applyAttributes(
|
||||
_ attributes: [HTMLAttribute: String],
|
||||
with transaction: Transaction
|
||||
) {
|
||||
// FIXME: is there a sensible way to diff attributes and listeners to avoid
|
||||
// crossing the JavaScript bridge and touching DOM if not needed?
|
||||
|
||||
// @carson-katri: For diffing, could you build a Set from the keys and values of the dictionary,
|
||||
// then use the standard lib to get the difference?
|
||||
|
||||
// `checked` attribute on checkboxes is a special one as its value doesn't matter. We only
|
||||
// need to check whether it exists or not, and set the property if it doesn't.
|
||||
for (attribute, value) in attributes {
|
||||
// Animate styles with the Web Animations API in `animateStyles`.
|
||||
guard transaction.disablesAnimations
|
||||
|| transaction.animation == nil
|
||||
|| attribute != "style"
|
||||
else { continue }
|
||||
|
||||
if attribute == "style" { // Clear animations
|
||||
ref.getAnimations?().array?.forEach { _ = $0.cancel() }
|
||||
}
|
||||
|
||||
if attribute.isUpdatedAsProperty {
|
||||
ref[dynamicMember: attribute.value] = .string(value)
|
||||
} else {
|
||||
_ = ref.setAttribute!(attribute.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractStyles(compute: Bool = false) -> [String: String] {
|
||||
var res = [String: String]()
|
||||
let computedStyle = JSObject.global.getComputedStyle?(ref)
|
||||
for i in 0..<Int(ref.style.object?.length.number ?? 0) {
|
||||
guard let key = ref.style.object?[i].string else { continue }
|
||||
if compute {
|
||||
res[key] = computedStyle?[dynamicMember: key].string
|
||||
?? ref.style.object?[key].string
|
||||
} else {
|
||||
res[key] = ref.style.object?[key].string
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func animate(
|
||||
keyframes: [JSValue],
|
||||
with animation: Animation,
|
||||
offsetBy iterationStart: Double = 0
|
||||
) -> JSValue? {
|
||||
let resolved = _AnimationProxy(animation).resolve()
|
||||
return ref.animate?(
|
||||
keyframes.jsValue(),
|
||||
[
|
||||
"duration": (resolved.duration / resolved.speed) * 1000,
|
||||
"delay": resolved.delay * 1000,
|
||||
"easing": resolved.style.cssValue,
|
||||
"iterations": resolved.repeatStyle.jsValue,
|
||||
"direction": resolved.repeatStyle.autoreverses ? "alternate" : "normal",
|
||||
// Keep the last keyframe applied when done, and the first applied during a delay.
|
||||
"fill": "both",
|
||||
"iterationStart": iterationStart,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func animateStyles(
|
||||
to style: String,
|
||||
computeStart: Bool,
|
||||
with animation: Animation
|
||||
) {
|
||||
let resolved = _AnimationProxy(animation).resolve()
|
||||
|
||||
let startStyle = Dictionary(uniqueKeysWithValues: extractStyles(compute: computeStart).map {
|
||||
($0.animatableProperty, $1)
|
||||
}).jsValue()
|
||||
ref.style.object?.cssText = .string(style)
|
||||
let endStyle = Dictionary(uniqueKeysWithValues: extractStyles().map {
|
||||
($0.animatableProperty, $1)
|
||||
})
|
||||
|
||||
let keyframes: [JSValue]
|
||||
if case let .solver(solver) = resolved.style {
|
||||
// Compute styles at several intervals.
|
||||
var values = [[String: String]]()
|
||||
for iterationStart in stride(from: 0, to: 1, by: 0.01) {
|
||||
// Create and immediately cancel an animation after reading the computed values.
|
||||
if let animation = animate(
|
||||
keyframes: [startStyle, endStyle.jsValue()],
|
||||
with: Animation.linear(duration: resolved.duration).delay(resolved.delay),
|
||||
offsetBy: iterationStart
|
||||
)?.object,
|
||||
let computedStyle = JSObject.global.getComputedStyle?(ref)
|
||||
{
|
||||
values.append(Dictionary(
|
||||
uniqueKeysWithValues: endStyle.keys
|
||||
.compactMap { k in computedStyle[dynamicMember: k].string.map { (k, $0) } }
|
||||
))
|
||||
_ = animation.cancel?()
|
||||
}
|
||||
}
|
||||
// Solve the values
|
||||
keyframes = (0..<values.count).map { t in
|
||||
let offset = Double(t) / Double(values.count - 1)
|
||||
let solved = solver.solve(at: offset * (resolved.duration / resolved.speed))
|
||||
* Double(values.count - 1)
|
||||
var res = values[Int(solved)]
|
||||
res["offset"] = "\(offset)"
|
||||
return res.jsValue()
|
||||
} + [endStyle.jsValue()] // Add the end for good measure.
|
||||
} else {
|
||||
keyframes = [startStyle, endStyle.jsValue()]
|
||||
}
|
||||
// Animate the styles.
|
||||
animate(keyframes: keyframes, with: animation)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,10 +110,7 @@ final class DOMRenderer: Renderer {
|
|||
to parent: DOMNode,
|
||||
with host: MountedHost
|
||||
) -> DOMNode? {
|
||||
guard let anyHTML = mapAnyView(
|
||||
host.view,
|
||||
transform: { (html: AnyHTML) in html }
|
||||
) else {
|
||||
guard let anyHTML: AnyHTML = mapAnyView(host.view, transform: { $0 }) else {
|
||||
// handle `GroupView` cases (such as `TupleView`, `Group` etc)
|
||||
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
|
||||
return parent
|
||||
|
@ -122,17 +119,49 @@ final class DOMRenderer: Renderer {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Transition the insertion.
|
||||
let transition = _AnyTransitionProxy(host.viewTraits.transition)
|
||||
.resolve(in: host.environmentValues)
|
||||
var additionalAttributes = [HTMLAttribute: String]()
|
||||
var runTransition: ((DOMNode) -> ())?
|
||||
if host.viewTraits.canTransition,
|
||||
let animation = transition.insertionAnimation ?? host.transaction.animation
|
||||
{
|
||||
// Apply the active insertion modifier on mount.
|
||||
additionalAttributes = apply(
|
||||
transition: transition, \.insertion,
|
||||
as: \.active,
|
||||
to: host.view
|
||||
)
|
||||
runTransition = { node in
|
||||
anyHTML.update(
|
||||
dom: node,
|
||||
computeStart: false,
|
||||
additionalAttributes: self.apply(
|
||||
transition: transition, \.insertion,
|
||||
as: \.identity,
|
||||
to: host.view
|
||||
),
|
||||
transaction: .init(animation: animation)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let maybeNode: JSObject?
|
||||
if let sibling = sibling {
|
||||
_ = sibling.ref.insertAdjacentHTML!(
|
||||
"beforebegin",
|
||||
anyHTML.outerHTML(shouldSortAttributes: false, children: [])
|
||||
anyHTML.outerHTML(
|
||||
shouldSortAttributes: false, additonalAttributes: additionalAttributes, children: []
|
||||
)
|
||||
)
|
||||
maybeNode = sibling.ref.previousSibling.object
|
||||
} else {
|
||||
_ = parent.ref.insertAdjacentHTML!(
|
||||
"beforeend",
|
||||
anyHTML.outerHTML(shouldSortAttributes: false, children: [])
|
||||
anyHTML.outerHTML(
|
||||
shouldSortAttributes: false, additonalAttributes: additionalAttributes, children: []
|
||||
)
|
||||
)
|
||||
|
||||
guard
|
||||
|
@ -148,18 +177,21 @@ final class DOMRenderer: Renderer {
|
|||
|
||||
fixSpacers(host: host, target: resultingNode)
|
||||
|
||||
if let dynamicHTML = anyHTML as? AnyDynamicHTML {
|
||||
return DOMNode(host.view, resultingNode, dynamicHTML.listeners)
|
||||
} else {
|
||||
return DOMNode(host.view, resultingNode, [:])
|
||||
}
|
||||
let node = DOMNode(host.view, resultingNode, (anyHTML as? AnyDynamicHTML)?.listeners ?? [:])
|
||||
|
||||
runTransition?(node)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(target: DOMNode, with host: MountedHost) {
|
||||
guard let html = mapAnyView(host.view, transform: { (html: AnyHTML) in html })
|
||||
else { return }
|
||||
|
||||
html.update(dom: target, transaction: host.transaction)
|
||||
html.update(
|
||||
dom: target,
|
||||
additionalAttributes: [:],
|
||||
transaction: host.transaction
|
||||
)
|
||||
|
||||
fixSpacers(host: host, target: target.ref)
|
||||
}
|
||||
|
@ -167,15 +199,55 @@ final class DOMRenderer: Renderer {
|
|||
func unmount(
|
||||
target: DOMNode,
|
||||
from parent: DOMNode,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
with task: UnmountHostTask<DOMRenderer>
|
||||
) {
|
||||
defer { completion() }
|
||||
guard let anyHTML = mapAnyView(task.host.view, transform: { (html: AnyHTML) in html })
|
||||
else { return task.finish() }
|
||||
|
||||
guard mapAnyView(host.view, transform: { (html: AnyHTML) in html }) != nil
|
||||
else { return }
|
||||
// Transition the removal.
|
||||
let transition = _AnyTransitionProxy(task.host.viewTraits.transition)
|
||||
.resolve(in: task.host.environmentValues)
|
||||
if task.host.viewTraits.canTransition,
|
||||
let animation = transition.removalAnimation ?? task.host.transaction.animation
|
||||
{
|
||||
// First, apply the identity removal modifier /without/ animation
|
||||
// to be in the initial state.
|
||||
anyHTML.update(
|
||||
dom: target,
|
||||
additionalAttributes: apply(
|
||||
transition: transition, \.removal,
|
||||
as: \.identity,
|
||||
to: task.host.view
|
||||
),
|
||||
transaction: .init(animation: nil)
|
||||
)
|
||||
|
||||
// Then apply the active removal modifier /with/ animation.
|
||||
anyHTML.update(
|
||||
dom: target,
|
||||
additionalAttributes: apply(
|
||||
transition: transition, \.removal,
|
||||
as: \.active,
|
||||
to: task.host.view
|
||||
),
|
||||
transaction: .init(animation: animation)
|
||||
)
|
||||
|
||||
_ = JSObject.global.setTimeout!(
|
||||
JSOneshotClosure { _ in
|
||||
guard !task.isCancelled else { return .undefined }
|
||||
_ = try? parent.ref.throwing.removeChild!(target.ref)
|
||||
task.finish()
|
||||
return .undefined
|
||||
},
|
||||
_AnimationProxy(animation).resolve().duration * 1000
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = try? parent.ref.throwing.removeChild!(target.ref)
|
||||
task.finish()
|
||||
}
|
||||
|
||||
func primitiveBody(for view: Any) -> AnyView? {
|
||||
|
@ -185,6 +257,33 @@ final class DOMRenderer: Renderer {
|
|||
func isPrimitiveView(_ type: Any.Type) -> Bool {
|
||||
type is DOMPrimitive.Type || type is _HTMLPrimitive.Type
|
||||
}
|
||||
|
||||
private func apply(
|
||||
transition: _AnyTransitionBox.ResolvedTransition,
|
||||
_ direction: KeyPath<
|
||||
_AnyTransitionBox.ResolvedTransition,
|
||||
[_AnyTransitionBox.ResolvedTransition.Transition]
|
||||
>,
|
||||
as state: KeyPath<
|
||||
_AnyTransitionBox.ResolvedTransition.Transition,
|
||||
(AnyView) -> AnyView
|
||||
>,
|
||||
to view: AnyView
|
||||
) -> [HTMLAttribute: String] {
|
||||
transition[keyPath: direction].reduce([HTMLAttribute: String]()) {
|
||||
if let modifiedContent = mapAnyView(
|
||||
$1[keyPath: state](view),
|
||||
transform: { (v: _AnyModifiedContent) in v }
|
||||
) {
|
||||
return $0.merging(
|
||||
modifiedContent.anyModifier.attributes,
|
||||
uniquingKeysWith: +
|
||||
)
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol DOMPrimitive {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/13/21.
|
||||
//
|
||||
|
||||
import TokamakCore
|
||||
import TokamakStaticHTML
|
||||
|
||||
extension _MoveTransition: DOMViewModifier {
|
||||
public var attributes: [HTMLAttribute: String] {
|
||||
let offset: (String, String)
|
||||
switch edge {
|
||||
case .leading: offset = ("-100%", "0%")
|
||||
case .trailing: offset = ("100%", "0%")
|
||||
case .top: offset = ("0%", "-100%")
|
||||
case .bottom: offset = ("0%", "100%")
|
||||
}
|
||||
return [
|
||||
"style":
|
||||
"transform: translate(\(isActive ? offset.0 : "0%"), \(isActive ? offset.1 : "0%"));",
|
||||
]
|
||||
}
|
||||
}
|
|
@ -137,6 +137,7 @@ struct TokamakDemoView: View {
|
|||
}
|
||||
Section(header: Text("Misc")) {
|
||||
NavItem("Animation", destination: AnimationDemo())
|
||||
NavItem("Transitions", destination: TransitionDemo())
|
||||
NavItem("Path", destination: PathDemo())
|
||||
NavItem("ProgressView", destination: ProgressViewDemo())
|
||||
NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8)))
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2019-2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Created by Carson Katri on 7/13/21.
|
||||
//
|
||||
|
||||
import TokamakShim
|
||||
|
||||
private struct ColorOverlayModifier: ViewModifier {
|
||||
var color: Color
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(color)
|
||||
}
|
||||
}
|
||||
|
||||
struct TransitionDemo: View {
|
||||
@State private var isVisible = false
|
||||
@State private var isInnerVisible = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Button(isVisible ? "Hide" : "Show") {
|
||||
withAnimation(.easeInOut(duration: 3)) {
|
||||
isVisible.toggle()
|
||||
}
|
||||
}
|
||||
if isVisible {
|
||||
Text(".opacity")
|
||||
.transition(AnyTransition.opacity)
|
||||
Text(".offset(x: 100, y: 100)")
|
||||
.transition(AnyTransition.offset(x: 100, y: 100))
|
||||
Text(".move(edge: .leading)")
|
||||
.transition(AnyTransition.move(edge: .leading))
|
||||
Text(".slide")
|
||||
.transition(AnyTransition.slide)
|
||||
Text(".scale")
|
||||
.transition(AnyTransition.scale)
|
||||
Text(".opacity/.scale")
|
||||
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .scale))
|
||||
Text(".opacity + .slide")
|
||||
.transition(AnyTransition.opacity.combined(with: .slide))
|
||||
Text(".modifier")
|
||||
.transition(AnyTransition.modifier(
|
||||
active: ColorOverlayModifier(color: .red),
|
||||
identity: ColorOverlayModifier(color: .clear)
|
||||
))
|
||||
Text(".animation")
|
||||
.transition(AnyTransition.scale.animation(.spring()))
|
||||
VStack {
|
||||
Text("Grouped Transition")
|
||||
Button(isInnerVisible ? "Hide Inner" : "Show Inner") {
|
||||
withAnimation(.easeInOut(duration: 3)) { isInnerVisible.toggle() }
|
||||
}
|
||||
Text(".slide").transition(AnyTransition.slide)
|
||||
if isInnerVisible {
|
||||
Text(".slide").transition(AnyTransition.slide)
|
||||
}
|
||||
}
|
||||
.transition(AnyTransition.slide)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -123,12 +123,11 @@ final class GTKRenderer: Renderer {
|
|||
func unmount(
|
||||
target: Widget,
|
||||
from parent: Widget,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
with task: UnmountHostTask<GTKRenderer>
|
||||
) {
|
||||
defer { completion() }
|
||||
defer { task.finish() }
|
||||
|
||||
guard mapAnyView(host.view, transform: { (widget: AnyWidget) in widget }) != nil
|
||||
guard mapAnyView(task.host.view, transform: { (widget: AnyWidget) in widget }) != nil
|
||||
else { return }
|
||||
|
||||
target.destroy()
|
||||
|
|
|
@ -14,17 +14,17 @@
|
|||
|
||||
import TokamakCore
|
||||
|
||||
private protocol AnyModifiedContent {
|
||||
public protocol _AnyModifiedContent {
|
||||
var anyContent: AnyView { get }
|
||||
var anyModifier: DOMViewModifier { get }
|
||||
}
|
||||
|
||||
extension ModifiedContent: AnyModifiedContent where Modifier: DOMViewModifier, Content: View {
|
||||
var anyContent: AnyView {
|
||||
extension ModifiedContent: _AnyModifiedContent where Modifier: DOMViewModifier, Content: View {
|
||||
public var anyContent: AnyView {
|
||||
AnyView(content)
|
||||
}
|
||||
|
||||
var anyModifier: DOMViewModifier {
|
||||
public var anyModifier: DOMViewModifier {
|
||||
modifier
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ extension ModifiedContent: _HTMLPrimitive where Content: View, Modifier: ViewMod
|
|||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
if let domModifier = modifier as? DOMViewModifier {
|
||||
if let adjacentModifier = content as? AnyModifiedContent,
|
||||
if let adjacentModifier = content as? _AnyModifiedContent,
|
||||
!(adjacentModifier.anyModifier.isOrderDependent || domModifier.isOrderDependent)
|
||||
{
|
||||
// Flatten non-order-dependent modifiers
|
||||
|
|
|
@ -48,7 +48,7 @@ extension _BackgroundStyleModifier: DOMViewModifier {
|
|||
]
|
||||
} else if let color = resolved.color(at: 0) {
|
||||
return [
|
||||
"style": "background-color: \(color.cssValue(environment))",
|
||||
"style": "background-color: \(color.cssValue(environment));",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,8 +136,7 @@ public final class StaticHTMLRenderer: Renderer {
|
|||
public func unmount(
|
||||
target: HTMLTarget,
|
||||
from parent: HTMLTarget,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
with host: UnmountHostTask<StaticHTMLRenderer>
|
||||
) {
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
|
|
|
@ -53,7 +53,12 @@ public protocol AnyHTML {
|
|||
}
|
||||
|
||||
public extension AnyHTML {
|
||||
func outerHTML(shouldSortAttributes: Bool, children: [HTMLTarget]) -> String {
|
||||
func outerHTML(
|
||||
shouldSortAttributes: Bool,
|
||||
additonalAttributes: [HTMLAttribute: String] = [:],
|
||||
children: [HTMLTarget]
|
||||
) -> String {
|
||||
let attributes = self.attributes.merging(additonalAttributes, uniquingKeysWith: +)
|
||||
let renderedAttributes: String
|
||||
if attributes.isEmpty {
|
||||
renderedAttributes = ""
|
||||
|
|
|
@ -35,12 +35,12 @@ struct _HTMLImage: View {
|
|||
case let .named(name, bundle):
|
||||
attributes = [
|
||||
"src": bundle?.path(forResource: name, ofType: nil) ?? name,
|
||||
"style": "max-width: 100%; max-height: 100%",
|
||||
"style": "max-width: 100%; max-height: 100%;",
|
||||
]
|
||||
case let .resizable(.named(name, bundle), _, _):
|
||||
attributes = [
|
||||
"src": bundle?.path(forResource: name, ofType: nil) ?? name,
|
||||
"style": "width: 100%; height: 100%",
|
||||
"style": "width: 100%; height: 100%;",
|
||||
]
|
||||
default: break
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ extension HStack: _HTMLPrimitive, SpacerContainer {
|
|||
align-items: \(alignment.cssValue);
|
||||
\(hasSpacer ? "width: 100%;" : "")
|
||||
\(fillCrossAxis ? "height: 100%;" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px;" : "")
|
||||
""",
|
||||
"class": "_tokamak-stack _tokamak-hstack",
|
||||
]) { content })
|
||||
|
|
|
@ -39,7 +39,7 @@ extension VStack: _HTMLPrimitive, SpacerContainer {
|
|||
justify-items: \(alignment.cssValue);
|
||||
\(hasSpacer ? "height: 100%;" : "")
|
||||
\(fillCrossAxis ? "width: 100%;" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px" : "")
|
||||
\(spacing != defaultStackSpacing ? "--tokamak-stack-gap: \(spacing)px;" : "")
|
||||
""",
|
||||
"class": "_tokamak-stack _tokamak-vstack",
|
||||
]) { content })
|
||||
|
|
|
@ -31,7 +31,7 @@ extension ZStack: _HTMLPrimitive {
|
|||
grid-template-columns: 1fr;
|
||||
width: fit-content;
|
||||
justify-items: \(alignment.horizontal.cssValue);
|
||||
align-items: \(alignment.vertical.cssValue)
|
||||
align-items: \(alignment.vertical.cssValue);
|
||||
""",
|
||||
]) {
|
||||
TupleView(
|
||||
|
|
|
@ -60,7 +60,7 @@ extension Spacer: _HTMLPrimitive {
|
|||
@_spi(TokamakStaticHTML)
|
||||
public var renderedBody: AnyView {
|
||||
AnyView(HTML("div", [
|
||||
"style": "flex-grow: 1; \(minLength != nil ? "min-width: \(minLength!)" : "")",
|
||||
"style": "flex-grow: 1; \(minLength != nil ? "min-width: \(minLength!);" : "")",
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,10 +68,10 @@ public final class TestRenderer: Renderer {
|
|||
public func unmount(
|
||||
target: TestView,
|
||||
from parent: TestView,
|
||||
with mountedHost: TestRenderer.MountedHost,
|
||||
completion: () -> ()
|
||||
with task: UnmountHostTask<TestRenderer>
|
||||
) {
|
||||
target.removeFromSuperview()
|
||||
task.finish()
|
||||
}
|
||||
|
||||
public func primitiveBody(for view: Any) -> AnyView? {
|
||||
|
|
Loading…
Reference in New Issue