From a591a43a7af532e5c40e214f886853cb292b3eff Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 18 Aug 2025 00:02:36 +0800 Subject: [PATCH] Add combineAnimation support --- .../Animatable/AnimatableAttribute.swift | 12 -- .../Animation/DefaultCombiningAnimation.swift | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Animation/Animation/DefaultCombiningAnimation.swift diff --git a/Sources/OpenSwiftUICore/Animation/Animatable/AnimatableAttribute.swift b/Sources/OpenSwiftUICore/Animation/Animatable/AnimatableAttribute.swift index 3f2ca1ab2..ff9ee6d7a 100644 --- a/Sources/OpenSwiftUICore/Animation/Animatable/AnimatableAttribute.swift +++ b/Sources/OpenSwiftUICore/Animation/Animatable/AnimatableAttribute.swift @@ -672,15 +672,3 @@ private struct FrameVelocityFilter { previous = (time, rect) } } - -// FIXME -func combineAnimation( - into: inout Animation, - state: inout AnimationState, - value: A, - elapsed: Double, - newAnimation: Animation, - newValue: A -) -> () where A: VectorArithmetic { - _openSwiftUIUnimplementedFailure() -} diff --git a/Sources/OpenSwiftUICore/Animation/Animation/DefaultCombiningAnimation.swift b/Sources/OpenSwiftUICore/Animation/Animation/DefaultCombiningAnimation.swift new file mode 100644 index 000000000..658af246d --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/Animation/DefaultCombiningAnimation.swift @@ -0,0 +1,111 @@ +// +// DefaultCombiningAnimation.swift +// OpenSwiftUICore +// +// Audited: 6.5.4 +// Status: Complete +// ID: 0E899C244938BDADF95265D65460D266 (SwiftUICore) + +import Foundation + +@_specialize(exported: false, kind: partial, where V == Double) +@_specialize(exported: false, kind: partial, where V == AnimatablePair, AnimatablePair>) +func combineAnimation( + into animation: inout Animation, + state: inout AnimationState, + value: V, + elapsed: Double, + newAnimation: Animation, + newValue: V +) where V: VectorArithmetic { + if var defaultCombiningAnimation = animation.as(DefaultCombiningAnimation.self) { + state.combinedState.entries.append(.init(value: value + newValue, state: .init())) + defaultCombiningAnimation.entries.append(.init(animation: newAnimation, elapsed: elapsed)) + animation = Animation(defaultCombiningAnimation) + } else { + var s = AnimationState() + s.combinedState.entries.append(.init(value: value, state: state)) + s.combinedState.entries.append(.init(value: value + newValue, state: .init())) + state = s + animation = Animation(DefaultCombiningAnimation(entries: [ + .init(animation: animation, elapsed: .zero), + .init(animation: newAnimation, elapsed: elapsed) + ])) + } +} + +extension AnimationState { + fileprivate var combinedState: CombinedAnimationState { + get { self[CombinedAnimationState.self] } + set { self[CombinedAnimationState.self] = newValue } + } +} + +struct CombinedAnimationState: AnimationStateKey where Value: VectorArithmetic { + static var defaultValue: Self { + .init(entries: []) + } + + struct Entry { + var value: Value + var state: AnimationState? + } + + var entries: [Entry] +} + +private struct DefaultCombiningAnimation: CustomAnimation { + struct Entry: Hashable { + var animation: Animation + var elapsed: Double + } + + var entries: [Entry] + + @_specialize(exported: false, kind: partial, where V == Double) + @_specialize(exported: false, kind: partial, where V == AnimatablePair, AnimatablePair>) + nonisolated func animate( + value: V, + time: TimeInterval, + context: inout AnimationContext + ) -> V? where V: VectorArithmetic { + let combinedStateEntryCount = context.state.combinedState.entries.count + guard combinedStateEntryCount == entries.count else { + return nil + } + var result: V = .zero + for index in 0 ..< combinedStateEntryCount { + let entry = entries[index] + guard let combinedStateEntryState = context.state.combinedState.entries[index].state else { + result = context.state.combinedState.entries[index].value + continue + } + var entryContext = context + entryContext.state = combinedStateEntryState + var entryValue = context.state.combinedState.entries[index].value + entryValue -= result + let elapsed = time - entry.elapsed + let entryAnimatedValue = entry.animation.animate( + value: entryValue, + time: elapsed, + context: &entryContext + ) + if let entryAnimatedValue { + context.state.combinedState.entries[index].state = entryContext.state + result += entryAnimatedValue + } else { + context.state.combinedState.entries[index].state = nil + result += entryValue + } + if index == combinedStateEntryCount - 1 { + context.isLogicallyComplete = entryContext.isLogicallyComplete + if entryAnimatedValue == nil { + return nil + } else { + return result + } + } + } + return nil + } +}