diff --git a/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableAnimation.swift
new file mode 100644
index 000000000..1d20470ca
--- /dev/null
+++ b/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableAnimation.swift
@@ -0,0 +1,91 @@
+//
+// CodableAnimation.swift
+// OpenSwiftUICore
+//
+// Audited for 6.5.4
+// Status: Complete
+
+// MARK: - CodableAnimation
+
+package struct CodableAnimation: ProtobufMessage {
+ package struct Tag: ProtobufTag {
+ package let rawValue: UInt
+
+ package init(rawValue: UInt) {
+ self.rawValue = rawValue
+ }
+ }
+
+ var base: Animation
+
+ init(base: Animation) {
+ self.base = base
+ }
+
+ package func encode(to encoder: inout ProtobufEncoder) throws {
+ let animation = base.codableValue as? any EncodableAnimation ?? DefaultAnimation()
+ try animation.encodeAnimation(to: &encoder)
+ }
+
+ package init(from decoder: inout ProtobufDecoder) throws {
+ var animation: Animation?
+ while let field = try decoder.nextField() {
+ switch field.tag {
+ case 1:
+ animation = Animation(try decoder.messageField(field) as BezierAnimation)
+ case 2:
+ animation = Animation(try decoder.messageField(field) as SpringAnimation)
+ case 3:
+ animation = Animation(try decoder.messageField(field) as FluidSpringAnimation)
+ case 4:
+ guard let existing = animation else { continue }
+ let delay = try decoder.doubleField(field)
+ animation = existing.modifier(DelayAnimation(delay: delay))
+ case 5:
+ let (repeatCount, autoreverses) = try decoder.messageField(field) { decoder in
+ try Animation.decodeRepeatMessage(from: &decoder)
+ }
+ guard let existing = animation else { continue }
+ animation = existing.modifier(
+ RepeatAnimation(repeatCount: repeatCount, autoreverses: autoreverses)
+ )
+ case 6:
+ guard let existing = animation else { continue }
+ let speed = try decoder.doubleField(field)
+ animation = existing.modifier(SpeedAnimation(speed: speed))
+ case 7:
+ animation = try decoder.messageField(field) { _ in
+ Animation(DefaultAnimation())
+ }
+ default:
+ try decoder.skipField(field)
+ }
+ }
+ guard let animation else {
+ throw ProtobufDecoder.DecodingError.failed
+ }
+ self.base = animation
+ }
+}
+
+// MARK: - Animation + decodeRepeatMessage
+
+extension Animation {
+ package static func decodeRepeatMessage(
+ from decoder: inout ProtobufDecoder
+ ) throws -> (Int?, Bool) {
+ var repeatCount: Int?
+ var autoreverses = false
+ while let field = try decoder.nextField() {
+ switch field.tag {
+ case 1:
+ repeatCount = try decoder.intField(field)
+ case 2:
+ autoreverses = try decoder.boolField(field)
+ default:
+ try decoder.skipField(field)
+ }
+ }
+ return (repeatCount, autoreverses)
+ }
+}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableEffectAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableEffectAnimation.swift
new file mode 100644
index 000000000..58a7f5c6a
--- /dev/null
+++ b/Sources/OpenSwiftUICore/Animation/Animation/Codable/CodableEffectAnimation.swift
@@ -0,0 +1,62 @@
+//
+// CodableEffectAnimation.swift
+// OpenSwiftUICore
+//
+// Audited for 6.5.4
+// Status: Complete
+
+// MARK: - CodableEffectAnimation
+
+package struct CodableEffectAnimation: ProtobufMessage {
+ package struct Tag: ProtobufTag {
+ package let rawValue: UInt
+
+ package init(rawValue: UInt) {
+ self.rawValue = rawValue
+ }
+ }
+
+ var base: any _DisplayList_AnyEffectAnimation
+
+ init(base: any _DisplayList_AnyEffectAnimation) {
+ self.base = base
+ }
+
+ package func encode(to encoder: inout ProtobufEncoder) throws {
+ try base.encodeAnimation(to: &encoder)
+ }
+
+ package init(from decoder: inout ProtobufDecoder) throws {
+ var base: (any _DisplayList_AnyEffectAnimation)?
+ while let field = try decoder.nextField() {
+ switch field.tag {
+ case 1:
+ base = try decoder.messageField(field) as DisplayList.OffsetAnimation
+ case 2:
+ base = try decoder.messageField(field) as DisplayList.ScaleAnimation
+ case 3:
+ base = try decoder.messageField(field) as DisplayList.RotationAnimation
+ case 4:
+ base = try decoder.messageField(field) as DisplayList.OpacityAnimation
+ default:
+ try decoder.skipField(field)
+ }
+ }
+ guard let base else {
+ throw ProtobufDecoder.DecodingError.failed
+ }
+ self.base = base
+ }
+}
+
+// MARK: - _DisplayList_AnyEffectAnimation + Encode
+
+extension _DisplayList_AnyEffectAnimation {
+ package func encodeAnimation(to encoder: inout ProtobufEncoder) throws {
+ if let leafTag = Self.leafProtobufTag {
+ try encoder.messageField(leafTag.rawValue, self)
+ } else {
+ try encode(to: &encoder)
+ }
+ }
+}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimation.swift
index 0ad320806..68a05be7a 100644
--- a/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimation.swift
+++ b/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimation.swift
@@ -206,3 +206,66 @@ extension CustomAnimation {
false
}
}
+
+// MARK: - EncodableAnimation
+
+package protocol EncodableAnimation: ProtobufEncodableMessage {
+ static var leafProtobufTag: CodableAnimation.Tag? { get }
+}
+
+extension EncodableAnimation {
+ package func encodeAnimation(to encoder: inout ProtobufEncoder) throws {
+ if let leafTag = Self.leafProtobufTag {
+ try encoder.messageField(leafTag.rawValue, self)
+ } else {
+ try encode(to: &encoder)
+ }
+ }
+}
+
+extension BezierAnimation: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? {
+ .init(rawValue: 1)
+ }
+}
+
+extension SpringAnimation: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? {
+ .init(rawValue: 2)
+ }
+}
+
+extension FluidSpringAnimation: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? {
+ .init(rawValue: 3)
+ }
+}
+
+extension DelayAnimation: ProtobufEncodableMessage {
+ package func encode(to encoder: inout ProtobufEncoder) throws {
+ encoder.doubleField(4, delay)
+ }
+}
+
+extension RepeatAnimation: ProtobufEncodableMessage {
+ package func encode(to encoder: inout ProtobufEncoder) throws {
+ encoder.messageField(5) { encoder in
+ if let repeatCount {
+ encoder.intField(1, repeatCount, defaultValue: .min)
+ }
+ encoder.boolField(2, autoreverses)
+ }
+ }
+}
+
+extension SpeedAnimation: ProtobufEncodableMessage {
+ package func encode(to encoder: inout ProtobufEncoder) throws {
+ encoder.doubleField(6, speed)
+ }
+}
+
+extension DefaultAnimation: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? {
+ .init(rawValue: 7)
+ }
+}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimationModifier.swift b/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimationModifier.swift
index c604e4320..01ec18f6e 100644
--- a/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimationModifier.swift
+++ b/Sources/OpenSwiftUICore/Animation/Animation/CustomAnimationModifier.swift
@@ -124,6 +124,8 @@ package struct CustomAnimationModifiedContent: InternalCustomAni
}
extension CustomAnimationModifiedContent: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? { nil }
+
package func encode(to encoder: inout ProtobufEncoder) throws {
let encodableAnimation: any EncodableAnimation
if let encodableBase = base as? EncodableAnimation {
@@ -206,6 +208,8 @@ package struct InternalCustomAnimationModifiedContent: InternalC
}
extension InternalCustomAnimationModifiedContent: EncodableAnimation {
+ package static var leafProtobufTag: CodableAnimation.Tag? { nil }
+
package func encode(to encoder: inout ProtobufEncoder) throws {
try _base.encode(to: &encoder)
}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/DelayAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/DelayAnimation.swift
index 90b15aaf0..39b72c49a 100644
--- a/Sources/OpenSwiftUICore/Animation/Animation/DelayAnimation.swift
+++ b/Sources/OpenSwiftUICore/Animation/Animation/DelayAnimation.swift
@@ -47,15 +47,15 @@ extension Animation {
}
}
-struct DelayAnimation: CustomAnimationModifier {
- var delay: TimeInterval
+package struct DelayAnimation: CustomAnimationModifier {
+ package var delay: TimeInterval
@inline(__always)
private func delayedTime(_ time: TimeInterval) -> TimeInterval {
max(0, time - delay)
}
- func animate(
+ package func animate(
base: B,
value: V,
time: TimeInterval,
@@ -68,7 +68,7 @@ struct DelayAnimation: CustomAnimationModifier {
)
}
- func shouldMerge(base: B, previous: DelayAnimation, previousBase: B, value: V, time: TimeInterval, context: inout AnimationContext) -> Bool where V : VectorArithmetic, B : CustomAnimation {
+ package func shouldMerge(base: B, previous: DelayAnimation, previousBase: B, value: V, time: TimeInterval, context: inout AnimationContext) -> Bool where V : VectorArithmetic, B : CustomAnimation {
self == previous && base.shouldMerge(
previous: Animation(previousBase),
value: value,
@@ -77,7 +77,7 @@ struct DelayAnimation: CustomAnimationModifier {
)
}
- func function(base: Animation.Function) -> Animation.Function {
+ package func function(base: Animation.Function) -> Animation.Function {
base
}
}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift
deleted file mode 100644
index 23e995173..000000000
--- a/Sources/OpenSwiftUICore/Animation/Animation/EncodableAnimation.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-//
-// EncodableAnimation.swift
-// OpenSwiftUICore
-//
-// Audited for 6.5.4
-// Status: WIP
-
-// MARK: - CodableAnimation [WIP]
-
-package enum CodableAnimation {
- package struct Tag: ProtobufTag {
- package let rawValue: UInt
-
- package init(rawValue: UInt) {
- self.rawValue = rawValue
- }
- }
-}
-
-// MARK: - EncodableAnimation
-
-package protocol EncodableAnimation: ProtobufEncodableMessage {
- static var leafProtobufTag: CodableAnimation.Tag? { get }
-}
-
-extension EncodableAnimation {
- package static var leafProtobufTag: CodableAnimation.Tag? { nil }
-
- package func encodeAnimation(to encoder: inout ProtobufEncoder) throws {
- if let leafTag = Self.leafProtobufTag {
- try encoder.messageField(leafTag.rawValue, self)
- } else {
- try encode(to: &encoder)
- }
- }
-}
-
-extension SpringAnimation: EncodableAnimation {
- package static var leafProtobufTag: CodableAnimation.Tag? {
- .init(rawValue: 2)
- }
-}
-
-extension FluidSpringAnimation: EncodableAnimation {
- package static var leafProtobufTag: CodableAnimation.Tag? {
- .init(rawValue: 3)
- }
-}
-
-extension RepeatAnimation: ProtobufEncodableMessage {
- func encode(to encoder: inout ProtobufEncoder) throws {
- encoder.messageField(5) { encoder in
- if let repeatCount {
- encoder.intField(1, repeatCount, defaultValue: .min)
- }
- encoder.boolField(2, autoreverses)
- }
- }
-}
-
-extension SpeedAnimation: ProtobufEncodableMessage {
- func encode(to encoder: inout ProtobufEncoder) throws {
- encoder.doubleField(6, speed)
- }
-}
-
-extension DefaultAnimation: EncodableAnimation {
- package static var leafProtobufTag: CodableAnimation.Tag? {
- .init(rawValue: 7)
- }
-}
diff --git a/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift
index f3d22be5a..13636d089 100644
--- a/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift
+++ b/Sources/OpenSwiftUICore/Animation/Animation/SpeedAnimation.swift
@@ -5,7 +5,7 @@
// Audited for 6.5.4
// Status: Complete
-import Foundation
+package import Foundation
// MARK: - View + speed Animation
@@ -83,15 +83,15 @@ extension Animation {
// MARK: - SpeedAnimation
-struct SpeedAnimation: CustomAnimationModifier {
- var speed: Double
+package struct SpeedAnimation: CustomAnimationModifier {
+ package var speed: Double
@inline(__always)
private func speededTime(_ time: TimeInterval) -> TimeInterval {
time * speed
}
- func animate(
+ package func animate(
base: B,
value: V,
time: TimeInterval,
@@ -104,7 +104,7 @@ struct SpeedAnimation: CustomAnimationModifier {
)
}
- func shouldMerge(
+ package func shouldMerge(
base: B,
previous: SpeedAnimation,
previousBase: B,
@@ -120,7 +120,7 @@ struct SpeedAnimation: CustomAnimationModifier {
)
}
- func function(base: Animation.Function) -> Animation.Function {
+ package func function(base: Animation.Function) -> Animation.Function {
.speed(speed, base)
}
}
diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift
index 3a030c77f..a01bfff14 100644
--- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift
+++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift
@@ -568,13 +568,23 @@ extension DisplayList {
package struct AccessibilityNodeAttachment {}
package protocol _DisplayList_AnyEffectAnimation: ProtobufMessage {
- // FIXME: CodableEffectAnimation
- static var leafProtobufTag: CodableAnimation.Tag? { get }
+ static var leafProtobufTag: CodableEffectAnimation.Tag? { get }
func makeAnimator() -> any _DisplayList_AnyEffectAnimator
}
package protocol _DisplayList_AnyEffectAnimator {
- func evaluate(
+ /// Evaluates the effect animation at the given time and viewport size.
+ ///
+ /// - Parameters:
+ /// - animation: The existential effect animation. Must be castable to
+ /// `Animation`; otherwise the method returns
+ /// ``DisplayList/Effect/identity`` with `finished: true`.
+ /// - time: The current render time.
+ /// - size: The viewport size passed to
+ /// ``EffectAnimation/effect(value:size:)``.
+ /// - Returns: A tuple of the resolved display list effect and a Boolean
+ /// indicating whether the animation has completed.
+ mutating func evaluate(
_ animation: any DisplayList.AnyEffectAnimation,
at time: Time,
size: CGSize
diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListAnimations.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListAnimations.swift
new file mode 100644
index 000000000..295a63870
--- /dev/null
+++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListAnimations.swift
@@ -0,0 +1,188 @@
+//
+// DisplayListAnimations.swift
+// OpenSwiftUICore
+//
+// Audited for 6.5.4
+// Status: Complete
+// ID: B86250B2E056EB47628ECF46032DFA4C (SwiftUICore)
+
+import Foundation
+
+// MARK: - EffectAnimation
+
+/// A private protocol that bridges animatable effect values to the display
+/// list animation system.
+///
+/// Conforming types represent a transition between two effect values (e.g.
+/// offset, scale, rotation, opacity) driven by a ``Animation``.
+///
+/// The protocol provides default implementations for ``ProtobufMessage``
+/// encoding/decoding and for ``_DisplayList_AnyEffectAnimator`` creation
+/// via ``EffectAnimator``.
+private protocol EffectAnimation: _DisplayList_AnyEffectAnimation {
+ /// The animatable effect value type
+ associatedtype Value: Animatable, ProtobufMessage
+
+ /// Creates an effect animation with the given endpoints and curve.
+ init(from: Value, to: Value, animation: Animation)
+
+ /// The starting effect value of the animation.
+ var from: Value { get }
+
+ /// The ending effect value of the animation.
+ var to: Value { get set }
+
+ /// The animation curve driving the transition from ``from`` to ``to``.
+ var animation: Animation { get }
+
+ /// Converts a current effect value and viewport size into a concrete
+ /// display list effect.
+ static func effect(value: Value, size: CGSize) -> DisplayList.Effect
+}
+
+extension EffectAnimation {
+ func makeAnimator() -> any _DisplayList_AnyEffectAnimator {
+ EffectAnimator(state: .pending)
+ }
+
+ func encode(to encoder: inout ProtobufEncoder) throws {
+ try encoder.messageField(1, from)
+ try encoder.messageField(2, to)
+ let codableAnimation = CodableAnimation(base: animation)
+ try encoder.messageField(3, codableAnimation)
+ }
+
+ init(from decoder: inout ProtobufDecoder) throws {
+ var fromValue: Value?
+ var toValue: Value?
+ var animationValue: Animation?
+ while let field = try decoder.nextField() {
+ switch field.tag {
+ case 1:
+ fromValue = try decoder.messageField(field)
+ case 2:
+ toValue = try decoder.messageField(field)
+ case 3:
+ let codable: CodableAnimation = try decoder.messageField(field)
+ animationValue = codable.base
+ default:
+ try decoder.skipField(field)
+ }
+ }
+ guard let fromValue, let toValue, let animationValue else {
+ throw ProtobufDecoder.DecodingError.failed
+ }
+ self.init(from: fromValue, to: toValue, animation: animationValue)
+ }
+}
+
+extension EffectAnimation where Value: GeometryEffect {
+ static func effect(value: Value, size: CGSize) -> DisplayList.Effect {
+ var origin = CGPoint.zero
+ return DefaultGeometryEffectProvider.resolve(
+ effect: value,
+ origin: &origin,
+ size: size,
+ layoutDirection: .leftToRight
+ )
+ }
+}
+
+// MARK: - DisplayList + Animation
+
+extension DisplayList {
+ struct OffsetAnimation: EffectAnimation {
+ static var leafProtobufTag: CodableEffectAnimation.Tag? { .init(rawValue: 1) }
+ var from: _OffsetEffect
+ var to: _OffsetEffect
+ var animation: Animation
+ }
+
+ struct ScaleAnimation: EffectAnimation {
+ static var leafProtobufTag: CodableEffectAnimation.Tag? { .init(rawValue: 2) }
+ var from: _ScaleEffect
+ var to: _ScaleEffect
+ var animation: Animation
+ }
+
+ struct RotationAnimation: EffectAnimation {
+ static var leafProtobufTag: CodableEffectAnimation.Tag? { .init(rawValue: 3) }
+ var from: _RotationEffect
+ var to: _RotationEffect
+ var animation: Animation
+ }
+
+ struct OpacityAnimation: EffectAnimation {
+ static var leafProtobufTag: CodableEffectAnimation.Tag? { .init(rawValue: 4) }
+ var from: _OpacityEffect
+ var to: _OpacityEffect
+ var animation: Animation
+
+ static func effect(value: _OpacityEffect, size: CGSize) -> DisplayList.Effect {
+ value.effectValue(size: size)
+ }
+ }
+}
+
+// MARK: - EffectAnimator
+
+/// Drives the frame-by-frame evaluation of an ``EffectAnimation``.
+private struct EffectAnimator: DisplayList.AnyEffectAnimator where Animation: EffectAnimation {
+ enum State {
+ /// No ``AnimatorState`` yet. On the first ``evaluate`` call the
+ /// animator computes the animatable interval
+ /// (`to.animatableData − from.animatableData`) and creates an
+ /// ``AnimatorState`` starting at `Time.zero`.
+ case pending
+
+ /// An ``AnimatorState`` exists and is updated each frame. The
+ /// animator interpolates the effect value and returns the
+ /// corresponding ``DisplayList/Effect``.
+ case active(AnimatorState)
+
+ /// ``AnimatorState/update`` returned `true`. The animator returns
+ /// the final `to` effect value with no further interpolation.
+ case finished
+ }
+
+ var state: State
+
+ mutating func evaluate(
+ _ animation: any DisplayList.AnyEffectAnimation,
+ at time: Time,
+ size: CGSize
+ ) -> (DisplayList.Effect, finished: Bool) {
+ guard let animation = animation as? Animation else {
+ return (.identity, true)
+ }
+
+ if case .pending = state {
+ let fromData = animation.from.animatableData
+ var interval = animation.to.animatableData
+ interval -= fromData
+ state = .active(AnimatorState(
+ animation: animation.animation,
+ interval: interval,
+ at: .zero,
+ in: Transaction()
+ ))
+ }
+ var toValue = animation.to
+ let finished: Bool
+ switch state {
+ case .pending:
+ _openSwiftUIUnreachableCode()
+ case let .active(animState):
+ var animatableData = toValue.animatableData
+ finished = animState.update(&animatableData, at: time, environment: nil)
+ if finished {
+ state = .finished
+ } else {
+ toValue.animatableData = animatableData
+ }
+ case .finished:
+ finished = true
+ }
+ return (Animation.effect(value: toValue, size: size), finished)
+ }
+}
diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift
index 99a3d3181..178e80bd6 100644
--- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift
+++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayListViewCache.swift
@@ -400,20 +400,17 @@ extension DisplayList.ViewUpdater {
let time = parentState.pointee.globals.pointee.time
var animatorInfo = animators[key, default: .init(state: .idle, deadline: .zero)]
if case .idle = animatorInfo.state {
- // If idle, initialize by creating an animator from the animation
animatorInfo.state = .active(animation.makeAnimator())
}
switch animatorInfo.state {
case let .active(animator):
- // Reset to idle before evaluation
+ var animator = animator
animatorInfo.state = .idle
- // Evaluate the animation effect
let (effect, finished) = animator.evaluate(
animation,
at: time,
size: item.size,
)
- // Swap item value with the animation effect
item.value = .effect(effect, displayList)
let maxVersion = parentState.pointee.globals.pointee.maxVersion
item.version = maxVersion
@@ -426,7 +423,6 @@ extension DisplayList.ViewUpdater {
animators[key] = animatorInfo
return finished ? .infinity : time
case let .finished(effect, version):
- // Re-apply the stored final effect
item.value = .effect(effect, displayList)
item.version = version
animatorInfo.deadline = time
diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Animation/CodableEffectAnimationTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Animation/CodableEffectAnimationTestsStub.c
new file mode 100644
index 000000000..d126e9df7
--- /dev/null
+++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Animation/CodableEffectAnimationTestsStub.c
@@ -0,0 +1,14 @@
+//
+// CodableAnimationTestsStub.c
+// OpenSwiftUISymbolDualTestsSupport
+
+#include "OpenSwiftUIBase.h"
+
+#if OPENSWIFTUI_TARGET_OS_DARWIN
+#import
+
+// CodableEffectAnimation
+DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_CodableEffectAnimationEncode, SwiftUICore, $s7SwiftUI22CodableEffectAnimationV6encode2toyAA15ProtobufEncoderVz_tKF);
+DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_CodableEffectAnimationDecode, SwiftUICore, $s7SwiftUI22CodableEffectAnimationV4fromAcA15ProtobufDecoderVz_tKcfC);
+
+#endif
diff --git a/Sources/OpenSwiftUITestsSupport/Data/Protobuf/ProtobufTestHelper.swift b/Sources/OpenSwiftUITestsSupport/Data/Protobuf/ProtobufTestHelper.swift
index 0cc70a0d0..9f2b1daf2 100644
--- a/Sources/OpenSwiftUITestsSupport/Data/Protobuf/ProtobufTestHelper.swift
+++ b/Sources/OpenSwiftUITestsSupport/Data/Protobuf/ProtobufTestHelper.swift
@@ -454,11 +454,26 @@ extension ProtobufEncodableMessage {
}
}
-extension ProtobufDecodableMessage where Self: Equatable {
+extension ProtobufDecodableMessage {
package func testPBDecoding(hexString: String) throws {
let decodedValue = try hexString.decodePBHexString(Self.self)
- #expect(decodedValue == self)
+ if let selfEquatable = self as? any Equatable,
+ let decodedEquatable = decodedValue as? any Equatable {
+ #expect(_pbIsEqual(selfEquatable, decodedEquatable))
+ } else if let decodedEncodable = decodedValue as? any ProtobufEncodableMessage {
+ let reEncoded = try ProtobufEncoder.encoding { encoder in
+ try decodedEncodable.encode(to: &encoder)
+ }
+ #expect(reEncoded.hexString == hexString)
+ }
+ }
+}
+
+private func _pbIsEqual(_ lhs: any Equatable, _ rhs: any Equatable) -> Bool {
+ func open(_ lhs: T) -> Bool {
+ lhs == (rhs as? T)
}
+ return open(lhs)
}
#endif
#endif
diff --git a/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableAnimationTests.swift b/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableAnimationTests.swift
new file mode 100644
index 000000000..b2fac2c59
--- /dev/null
+++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableAnimationTests.swift
@@ -0,0 +1,67 @@
+//
+// CodableAnimationTests.swift
+// OpenSwiftUICoreTests
+
+import Foundation
+@testable import OpenSwiftUICore
+import OpenSwiftUITestsSupport
+import Testing
+
+// MARK: - CodableAnimationTests
+
+struct CodableAnimationTests {
+ @Test(
+ arguments: [
+ ("default", CodableAnimation(base: Animation(DefaultAnimation())), "3a00"),
+ (
+ "bezier",
+ CodableAnimation(
+ base: Animation(BezierAnimation(
+ curve: .init(
+ startControlPoint: UnitPoint(x: 0.42, y: 0.0),
+ endControlPoint: UnitPoint(x: 0.58, y: 1.0)
+ ),
+ duration: 0.3
+ ))
+ ),
+ "0a2609333333333333d33f121b09e17a14ae47e1da3f198fc2f5285c8fe23f21000000000000f03f"
+ ),
+ (
+ "spring",
+ CodableAnimation(base: Animation(SpringAnimation(mass: 1.0, stiffness: 100.0, damping: 10.0))),
+ "1209190000000000002440"
+ ),
+ (
+ "fluidSpring",
+ CodableAnimation(
+ base: Animation(FluidSpringAnimation(response: 0.5, dampingFraction: 0.8, blendDuration: 0.0))
+ ),
+ "1a1209000000000000e03f119a9999999999e93f"
+ ),
+ (
+ "delay",
+ CodableAnimation(base: Animation(DefaultAnimation()).delay(0.5)),
+ "3a0021000000000000e03f"
+ ),
+ (
+ "repeatCount",
+ CodableAnimation(base: Animation(DefaultAnimation()).repeatCount(3, autoreverses: true)),
+ "3a002a0408061001"
+ ),
+ (
+ "repeatForever",
+ CodableAnimation(base: Animation(DefaultAnimation()).repeatForever(autoreverses: false)),
+ "3a002a00"
+ ),
+ (
+ "speed",
+ CodableAnimation(base: Animation(DefaultAnimation()).speed(2.0)),
+ "3a00310000000000000040"
+ ),
+ ] as [(String, CodableAnimation, String)]
+ )
+ func pbMessage(label: String, animation: CodableAnimation, hexString: String) throws {
+ try animation.testPBEncoding(hexString: hexString)
+ try animation.testPBDecoding(hexString: hexString)
+ }
+}
diff --git a/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableEffectAnimationTests.swift b/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableEffectAnimationTests.swift
new file mode 100644
index 000000000..fa3c563e5
--- /dev/null
+++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/CodableEffectAnimationTests.swift
@@ -0,0 +1,65 @@
+//
+// CodableEffectAnimationTests.swift
+// OpenSwiftUICoreTests
+
+import Foundation
+@testable import OpenSwiftUICore
+import OpenSwiftUITestsSupport
+import Testing
+
+// MARK: - CodableEffectAnimationTests
+
+struct CodableEffectAnimationTests {
+ @Test(
+ arguments: [
+ (
+ "offset",
+ CodableEffectAnimation(
+ base: DisplayList.OffsetAnimation(
+ from: _OffsetEffect(offset: .zero),
+ to: _OffsetEffect(offset: CGSize(width: 10, height: 20)),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "0a140a00120c0a0a0d00002041150000a0411a023a00"
+ ),
+ (
+ "scale",
+ CodableEffectAnimation(
+ base: DisplayList.ScaleAnimation(
+ from: _ScaleEffect(scale: CGSize(width: 1, height: 1), anchor: .center),
+ to: _ScaleEffect(scale: CGSize(width: 2, height: 2), anchor: .center),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "12140a00120c0a0a0d0000004015000000401a023a00"
+ ),
+ (
+ "rotation",
+ CodableEffectAnimation(
+ base: DisplayList.RotationAnimation(
+ from: _RotationEffect(angle: .zero, anchor: .center),
+ to: _RotationEffect(angle: .degrees(90), anchor: .center),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "1a110a00120909182d4454fb21f93f1a023a00"
+ ),
+ (
+ "opacity",
+ CodableEffectAnimation(
+ base: DisplayList.OpacityAnimation(
+ from: _OpacityEffect(opacity: 0.0),
+ to: _OpacityEffect(opacity: 1.0),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "220d0a050d0000000012001a023a00"
+ ),
+ ] as [(String, CodableEffectAnimation, String)]
+ )
+ func pbMessage(label: String, value: CodableEffectAnimation, hexString: String) throws {
+ try value.testPBEncoding(hexString: hexString)
+ try value.testPBDecoding(hexString: hexString)
+ }
+}
diff --git a/Tests/OpenSwiftUISymbolDualTests/Animation/CodableEffectAnimationDualTests.swift b/Tests/OpenSwiftUISymbolDualTests/Animation/CodableEffectAnimationDualTests.swift
new file mode 100644
index 000000000..f5c829cd6
--- /dev/null
+++ b/Tests/OpenSwiftUISymbolDualTests/Animation/CodableEffectAnimationDualTests.swift
@@ -0,0 +1,102 @@
+//
+// CodableAnimationDualTests.swift
+// OpenSwiftUISymbolDualTests
+
+#if canImport(SwiftUI, _underlyingVersion: 6.5.4)
+import Foundation
+@testable import OpenSwiftUICore
+import OpenSwiftUITestsSupport
+import Testing
+
+// MARK: - @_silgen_name declarations
+
+extension CodableEffectAnimation {
+ @_silgen_name("OpenSwiftUITestStub_CodableEffectAnimationEncode")
+ func swiftUI_encode(to encoder: inout ProtobufEncoder) throws
+
+ @_silgen_name("OpenSwiftUITestStub_CodableEffectAnimationDecode")
+ init(swiftUI_from decoder: inout ProtobufDecoder) throws
+}
+
+// MARK: - CodableEffectAnimation Dual Tests
+
+@Suite
+struct CodableEffectAnimationDualTests {
+ @Test(
+ arguments: [
+ (
+ "offset",
+ CodableEffectAnimation(
+ base: DisplayList.OffsetAnimation(
+ from: _OffsetEffect(offset: .zero),
+ to: _OffsetEffect(offset: CGSize(width: 10, height: 20)),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "0a140a00120c0a0a0d00002041150000a0411a023a00"
+ ),
+ (
+ "scale",
+ CodableEffectAnimation(
+ base: DisplayList.ScaleAnimation(
+ from: _ScaleEffect(scale: CGSize(width: 1, height: 1), anchor: .center),
+ to: _ScaleEffect(scale: CGSize(width: 2, height: 2), anchor: .center),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "12140a00120c0a0a0d0000004015000000401a023a00"
+ ),
+ (
+ "rotation",
+ CodableEffectAnimation(
+ base: DisplayList.RotationAnimation(
+ from: _RotationEffect(angle: .zero, anchor: .center),
+ to: _RotationEffect(angle: .degrees(90), anchor: .center),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "1a110a00120909182d4454fb21f93f1a023a00"
+ ),
+ (
+ "opacity",
+ CodableEffectAnimation(
+ base: DisplayList.OpacityAnimation(
+ from: _OpacityEffect(opacity: 0.0),
+ to: _OpacityEffect(opacity: 1.0),
+ animation: Animation(DefaultAnimation())
+ )
+ ),
+ "220d0a050d0000000012001a023a00"
+ ),
+ ] as [(String, CodableEffectAnimation, String)]
+ )
+ func pbMessage(label: String, value: CodableEffectAnimation, hexString: String) throws {
+ try value.testPBEncoding(swiftUI_hexString: hexString)
+ try value.testPBDecoding(swiftUI_hexString: hexString)
+ }
+}
+
+// MARK: - SwiftUI Dual Test Helpers
+
+extension CodableEffectAnimation {
+ func testPBEncoding(swiftUI_hexString expectedHexString: String) throws {
+ let data = try ProtobufEncoder.encoding { encoder in
+ try swiftUI_encode(to: &encoder)
+ }
+ #expect(data.hexString == expectedHexString)
+ }
+
+ func testPBDecoding(swiftUI_hexString hexString: String) throws {
+ guard let data = Data(hexString: hexString) else {
+ throw ProtobufDecoder.DecodingError.failed
+ }
+ var decoder = ProtobufDecoder(data)
+ let decoded = try CodableEffectAnimation(swiftUI_from: &decoder)
+ let reEncoded = try ProtobufEncoder.encoding { encoder in
+ try decoded.swiftUI_encode(to: &encoder)
+ }
+ #expect(reEncoded.hexString == hexString)
+ }
+}
+
+#endif