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