From 10d756075d3d7cdb6122ca821bccb6c87412aa9d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 24 Oct 2026 01:23:28 +0800 Subject: [PATCH 1/5] feat: add AnyEquatable value storage --- .../Data/Util/AnyEquatable.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Data/Util/AnyEquatable.swift diff --git a/Sources/OpenSwiftUICore/Data/Util/AnyEquatable.swift b/Sources/OpenSwiftUICore/Data/Util/AnyEquatable.swift new file mode 100644 index 000000000..cf722257e --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Util/AnyEquatable.swift @@ -0,0 +1,79 @@ +// +// AnyEquatable.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 5FED8E8437E1C5DEB2554486604823AB (SwiftUICore) + +// MARK: - AnyEquatable + +package struct AnyEquatable: Equatable { + private var box: AnyEquatableBox + + package init(_ value: T) where T: Equatable { + box = _AnyEquatableBox(value) + } + + package init(hash: StrongHash) { + box = HashEquatableBox(value: hash) + } + + package var hash: StrongHash? { + box.hash + } + + package static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool { + lhs.box === rhs.box || lhs.box.isEqual(to: rhs.box) + } +} + +// MARK: - AnyEquatableBox + +private class AnyEquatableBox { + func isEqual(to other: AnyEquatableBox) -> Bool { + _openSwiftUIBaseClassAbstractMethod() + } + + var hash: StrongHash? { + nil + } +} + +private final class _AnyEquatableBox: AnyEquatableBox where Value: Equatable { + let value: Value + + init(_ value: Value) { + self.value = value + } + + override func isEqual(to other: AnyEquatableBox) -> Bool { + guard let other = other as? _AnyEquatableBox else { + return false + } + return value == other.value + } + + override var hash: StrongHash? { + makeStableIDData(from: value) + } +} + +private final class HashEquatableBox: AnyEquatableBox { + let value: StrongHash + + init(value: StrongHash) { + self.value = value + } + + override func isEqual(to other: AnyEquatableBox) -> Bool { + guard let other = other as? HashEquatableBox else { + return false + } + return value == other.value + } + + override var hash: StrongHash? { + value + } +} From bcb8af233191157ee4a38c354feb0acbb82f22a5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 24 Oct 2026 11:13:40 +0800 Subject: [PATCH 2/5] fix: handle protobuf decoder boundary values --- .../Data/Protobuf/ProtobufDecoder.swift | 8 +- .../Data/Protobuf/ProtobufDecoderTests.swift | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufDecoder.swift b/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufDecoder.swift index 4ebb29045..18d18cfaa 100644 --- a/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufDecoder.swift +++ b/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufDecoder.swift @@ -144,7 +144,7 @@ extension ProtobufDecoder { /// - Parameter field: The field to decode. /// - Returns: An unsigned 8-bit integer(UInt8) value. package mutating func uint8Field(_ field: ProtobufDecoder.Field) throws -> UInt8 { - try UInt8(uintField(field)) + try UInt8(truncatingIfNeeded: uint64Field(field)) } /// Decodes an unsigned 16-bit integer(UInt16) value field from the data. @@ -152,7 +152,7 @@ extension ProtobufDecoder { /// - Parameter field: The field to decode. /// - Returns: An unsigned 16-bit integer(UInt16) value. package mutating func uint16Field(_ field: ProtobufDecoder.Field) throws -> UInt16 { - try UInt16(uintField(field)) + try UInt16(truncatingIfNeeded: uint64Field(field)) } /// Decodes an unsigned 32-bit integer(UInt32) value field from the data. @@ -160,7 +160,7 @@ extension ProtobufDecoder { /// - Parameter field: The field to decode. /// - Returns: An unsigned 32-bit integer(UInt32) value. package mutating func uint32Field(_ field: ProtobufDecoder.Field) throws -> UInt32 { - try UInt32(uintField(field)) + try UInt32(truncatingIfNeeded: uint64Field(field)) } /// Decodes an unsigned 64-bit integer(UInt64) value field from the data. @@ -358,7 +358,7 @@ extension ProtobufDecoder { switch field.wireType { case .lengthDelimited: let buffer = try decodeDataBuffer() - guard let baseAddress = buffer.baseAddress else { + guard let baseAddress = buffer.baseAddress, !buffer.isEmpty else { return Data() } let startIndex = baseAddress - data.bytes diff --git a/Tests/OpenSwiftUICoreTests/Data/Protobuf/ProtobufDecoderTests.swift b/Tests/OpenSwiftUICoreTests/Data/Protobuf/ProtobufDecoderTests.swift index d707ea88e..1780138f2 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Protobuf/ProtobufDecoderTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Protobuf/ProtobufDecoderTests.swift @@ -41,6 +41,64 @@ struct ProtobufDecoderTests { #expect(try "108001".decodePBHexString(IntegerMessage.self) == IntegerMessage(unsignedIntValue: 0x80)) #expect(try "20ffffffffffffffffff01".decodePBHexString(IntegerMessage.self) == IntegerMessage(unsignedInt64Value: .max)) } + + @Test(arguments: [ + ([0x00], 0, 0, 0), + ([0xff, 0x01], 255, 255, 255), + ([0x80, 0x02], 0, 256, 256), + ([0x81, 0x02], 1, 257, 257), + ([0xff, 0xff, 0x03], 255, 65535, 65535), + ([0x80, 0x80, 0x04], 0, 0, 65536), + ([0x81, 0x80, 0x04], 1, 1, 65537), + ([0xff, 0xff, 0xff, 0xff, 0x0f], 255, 65535, 4294967295), + ([0x80, 0x80, 0x80, 0x80, 0x10], 0, 0, 0), + ([0x81, 0x80, 0x80, 0x80, 0x10], 1, 1, 1), + ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], 255, 65535, 4294967295), + ] as [([UInt8], UInt8, UInt16, UInt32)]) + func unsignedIntegerDecode(bytes: [UInt8], expected8: UInt8, expected16: UInt16, expected32: UInt32) throws { + let data = Data([0x08] + bytes) + var decoder = ProtobufDecoder(data) + var field = try #require(try decoder.nextField()) + #expect(try decoder.uint8Field(field) == expected8) + #expect(try decoder.nextField() == nil) + + decoder = ProtobufDecoder(data) + field = try #require(try decoder.nextField()) + #expect(try decoder.uint16Field(field) == expected16) + #expect(try decoder.nextField() == nil) + + decoder = ProtobufDecoder(data) + field = try #require(try decoder.nextField()) + #expect(try decoder.uint32Field(field) == expected32) + #expect(try decoder.nextField() == nil) + } + + @Test(arguments: [ + [0x08, 0x80], + [0x0a, 0x02, 0x80], + [0x0a, 0x01, 0x80], + [0x0d, 0x00, 0x00, 0x00, 0x00], + ] as [[UInt8]]) + func invalidUnsignedIntegerDecode(bytes: [UInt8]) throws { + let data = Data(bytes) + var decoder = ProtobufDecoder(data) + var field = try #require(try decoder.nextField()) + #expect(throws: ProtobufDecoder.DecodingError.failed) { + try decoder.uint8Field(field) + } + + decoder = ProtobufDecoder(data) + field = try #require(try decoder.nextField()) + #expect(throws: ProtobufDecoder.DecodingError.failed) { + try decoder.uint16Field(field) + } + + decoder = ProtobufDecoder(data) + field = try #require(try decoder.nextField()) + #expect(throws: ProtobufDecoder.DecodingError.failed) { + try decoder.uint32Field(field) + } + } @Test func skipInvalidTagDecode() throws { @@ -90,6 +148,55 @@ struct ProtobufDecoderTests { #expect(try "0a7f\(payload127)".decodePBHexString(DataMessage.self) == DataMessage(data: .init(repeating: 0xFF, count: 0x7F))) #expect(try "0a8001\(payload128)".decodePBHexString(DataMessage.self) == DataMessage(data: .init(repeating: 0xFF, count: 0x80))) } + + @Test(arguments: [false, true]) + func emptyDataDecode(hasFollowingField: Bool) throws { + let bytes: [UInt8] = hasFollowingField ? [0x0a, 0x00, 0x10, 0x01] : [0x0a, 0x00] + var decoder = ProtobufDecoder(Data(bytes)) + let field = try #require(try decoder.nextField()) + let data = try decoder.dataField(field) + #expect(data.isEmpty) + #expect(data.startIndex == 0) + #expect(data.endIndex == 0) + if hasFollowingField { + let next = try #require(try decoder.nextField()) + #expect(next.tag == 2) + #expect(try decoder.boolField(next)) + } + #expect(try decoder.nextField() == nil) + } + + @Test + func dataSliceDecode() throws { + var decoder = ProtobufDecoder(Data([0x0a, 0x02, 0xab, 0xcd, 0x12, 0x00])) + let field = try #require(try decoder.nextField()) + let data = try decoder.dataField(field) + #expect(data == Data([0xab, 0xcd])) + #expect(data.startIndex == 2) + #expect(data.endIndex == 4) + + let next = try #require(try decoder.nextField()) + #expect(next.tag == 2) + let empty = try decoder.dataField(next) + #expect(empty.isEmpty) + #expect(empty.startIndex == 0) + #expect(empty.endIndex == 0) + #expect(try decoder.nextField() == nil) + } + + @Test(arguments: [ + [0x0a], + [0x0a, 0x80], + [0x0a, 0x02, 0xff], + [0x08, 0x00], + ] as [[UInt8]]) + func invalidDataDecode(bytes: [UInt8]) throws { + var decoder = ProtobufDecoder(Data(bytes)) + let field = try #require(try decoder.nextField()) + #expect(throws: ProtobufDecoder.DecodingError.failed) { + try decoder.dataField(field) + } + } @Test func packedDecode() throws { @@ -97,6 +204,41 @@ struct ProtobufDecoderTests { #expect(try "0a0400108002".decodePBHexString(PackedIntMessage.self).values == [0, 8, 128]) #expect(try "0a020010".decodePBHexString(PackedIntMessage.self).values == [0, 8]) } + + @Test + func packedUnsignedIntegerDecode() throws { + var decoder = ProtobufDecoder(Data([ + 0x0a, 0x0d, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + 0x80, 0x02, 0x01, + 0x12, 0x0e, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + 0x80, 0x80, 0x04, 0x01, + 0x1a, 0x10, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + 0x80, 0x80, 0x80, 0x80, 0x10, 0x01, + 0x20, 0x01, + ])) + for expected in [255, 0, 1] as [UInt8] { + let field = try #require(try decoder.nextField()) + #expect(field.tag == 1) + #expect(try decoder.uint8Field(field) == expected) + } + for expected in [65535, 0, 1] as [UInt16] { + let field = try #require(try decoder.nextField()) + #expect(field.tag == 2) + #expect(try decoder.uint16Field(field) == expected) + } + for expected in [4294967295, 0, 1] as [UInt32] { + let field = try #require(try decoder.nextField()) + #expect(field.tag == 3) + #expect(try decoder.uint32Field(field) == expected) + } + let field = try #require(try decoder.nextField()) + #expect(field.tag == 4) + #expect(try decoder.boolField(field)) + #expect(try decoder.nextField() == nil) + } @Test func messageDecode() throws { From 98448a6ed05927436f14a01c602c2551742a1478 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 24 Oct 2026 11:46:14 +0800 Subject: [PATCH 3/5] feat: implement _SymbolEffect --- .../OpenSwiftUICore/Render/SymbolEffect.swift | 55 - .../Render/SymbolEffect/_SymbolEffect.swift | 982 ++++++++++++++++++ .../Render/SymbolEffectTests.swift | 100 ++ 3 files changed, 1082 insertions(+), 55 deletions(-) delete mode 100644 Sources/OpenSwiftUICore/Render/SymbolEffect.swift create mode 100644 Sources/OpenSwiftUICore/Render/SymbolEffect/_SymbolEffect.swift create mode 100644 Tests/OpenSwiftUICoreTests/Render/SymbolEffectTests.swift diff --git a/Sources/OpenSwiftUICore/Render/SymbolEffect.swift b/Sources/OpenSwiftUICore/Render/SymbolEffect.swift deleted file mode 100644 index 3f70711c4..000000000 --- a/Sources/OpenSwiftUICore/Render/SymbolEffect.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SymbolEffect.swift -// OpenSwiftUICore -// -// Status: Empty - -import OpenAttributeGraphShims -package import OpenRenderBoxShims - -package struct _SymbolEffect: Equatable { - - package struct ReplaceConfiguration: Equatable { - package init() { - // TODO - } - } -} - -extension _SymbolEffect { - package struct Identified: Equatable { - - } - - package struct Phase: Equatable { - package init() { - // TODO - } - } -} - -extension EnvironmentValues { - package var symbolEffects: [_SymbolEffect.Identified] { - get { _openSwiftUIUnimplementedFailure() } - set { _openSwiftUIUnimplementedFailure() } - } - - package mutating func appendSymbolEffect( - _ effect: _SymbolEffect, - for identifier: Int - ) { - _openSwiftUIUnimplementedFailure() - } -} - -extension GraphicsImage { - mutating func updateSymbolEffects( - _ phase: inout _SymbolEffect.Phase, - environment: EnvironmentValues, - transaction: Attribute, - animationsDisabled: Bool - ) -> ORBSymbolAnimator? { - _openSwiftUIUnimplementedWarning() - return nil - } -} diff --git a/Sources/OpenSwiftUICore/Render/SymbolEffect/_SymbolEffect.swift b/Sources/OpenSwiftUICore/Render/SymbolEffect/_SymbolEffect.swift new file mode 100644 index 000000000..cf0d7162d --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/SymbolEffect/_SymbolEffect.swift @@ -0,0 +1,982 @@ +// +// _SymbolEffect.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 2975F89CBD28662DFA5DA6D958CBE343 (SwiftUICore) + +import OpenAttributeGraphShims +package import OpenRenderBoxShims + +// MARK: - _SymbolEffect + +package struct _SymbolEffect: Equatable { + package enum Storage: Equatable { + case pulse(_SymbolEffect.PulseConfiguration) + case breathe(_SymbolEffect.BreatheConfiguration) + case bounce(_SymbolEffect.BounceConfiguration) + case wiggle(_SymbolEffect.WiggleConfiguration) + case rotate(_SymbolEffect.RotateConfiguration) + case variableColor(_SymbolEffect.VariableColorConfiguration) + case scale(_SymbolEffect.ScaleConfiguration) + case hidden(_SymbolEffect.HiddenConfiguration) + case none + } + + package enum Trigger: Equatable { + case value(AnyEquatable) + case condition(Bool) + + package func activates(after oldTrigger: _SymbolEffect.Trigger?) -> Bool { + guard let oldTrigger else { + return false + } + switch (self, oldTrigger) { + case let (.value(newValue), .value(oldValue)): + return oldValue != newValue + case let (.condition(newValue), .condition(oldValue)): + return !oldValue && newValue + default: + return false + } + } + } + + package struct Options: Equatable { + package var speed: Float + + package var repeatCount: Float? + + package var repeatDelay: Float? + + package init() { + speed = 1.0 + repeatCount = nil + repeatDelay = nil + } + } + + package var storage: _SymbolEffect.Storage + + package var options: _SymbolEffect.Options + + package init( + _ storage: _SymbolEffect.Storage, + options: _SymbolEffect.Options = .init() + ) { + self.storage = storage + self.options = options + } + + package struct PulseConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var layered: Bool + + package init() { + trigger = nil + layered = true + } + } + + package struct BreatheConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var flags: ORBSymbolAnimation.BreatheOptions + + package var layered: Bool + + package init() { + trigger = nil + flags = [] + layered = true + } + } + + package struct BounceConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var flags: ORBSymbolAnimation.BounceOptions + + package var layered: Bool + + package init() { + trigger = nil + flags = [] + layered = true + } + } + + package struct WiggleConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var angle: Angle? + + package var flags: ORBSymbolAnimation.WiggleOptions + + package var layered: Bool + + package var appliesRTL: Bool + + package init() { + trigger = nil + angle = nil + flags = [] + layered = true + appliesRTL = false + } + } + + package struct RotateConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var flags: ORBSymbolAnimation.RotateOptions + + package var layered: Bool + + package init() { + trigger = nil + flags = [] + layered = true + } + } + + package struct VariableColorConfiguration: Equatable { + package var trigger: _SymbolEffect.Trigger? + + package var flags: ORBSymbolAnimation.VariableColorOptions + + package init() { + trigger = nil + flags = [] + } + } + + package struct ScaleConfiguration: Equatable { + package var level: ORBSymbolScaleLevel + + package var layered: Bool + + package init() { + level = .normal + layered = true + } + } + + package struct HiddenConfiguration: Equatable { + package var flags: ORBSymbolAnimation.AppearDisappearOptions + + package var layered: Bool + + package var isHidden: Bool + + package init() { + flags = [] + layered = true + isHidden = false + } + + package var transitionFlags: ORBSymbolAnimation.AppearDisappearOptions { + layered ? flags : flags.union(.wholeSymbol) + } + } + + package struct ReplaceConfiguration: Equatable { + package var flags: ORBSymbolAnimation.ReplaceOptions + + package var layered: Bool + + package var speed: Float + + package init() { + flags = [] + layered = true + speed = 1.0 + } + + package var transitionFlags: ORBSymbolAnimation.ReplaceOptions { + layered ? flags : flags.union(.wholeSymbol) + } + } +} + +extension _SymbolEffect { + package struct Identified: Equatable { + package var identifier: Int + + package var serial: Int + + package var effect: _SymbolEffect + + package init(identifier: Int, serial: Int = 0, effect: _SymbolEffect) { + self.identifier = identifier + self.serial = serial + self.effect = effect + } + } + + package struct Phase: Equatable { + fileprivate var added: [_SymbolEffect.Added] + + fileprivate var scale: _SymbolEffect.ScaleConfiguration + + fileprivate var scaleOptions: _SymbolEffect.Options + + fileprivate var hidden: _SymbolEffect.HiddenConfiguration + + fileprivate var hiddenOptions: _SymbolEffect.Options + + fileprivate var isFirstUpdate: Bool + + package init() { + added = [] + scale = .init() + scaleOptions = .init() + hidden = .init() + hiddenOptions = .init() + isFirstUpdate = true + } + } + + fileprivate struct Added: Equatable { + var identifier: Int + + var serial: Int + + var effect: _SymbolEffect.Storage + + var animationID: UInt32 + + var priority: Float + } + + fileprivate mutating func applyLayoutDirection(from environment: EnvironmentValues) { + guard case var .wiggle(configuration) = storage, + configuration.appliesRTL, + environment.layoutDirection == .rightToLeft else { + return + } + let direction = configuration.flags.intersection(.directionMask) + configuration.flags.subtract(.directionMask) + switch direction { + case [], .forward: + configuration.flags.formUnion(.backward) + case .backward: + configuration.flags.formUnion(.forward) + default: + break + } + configuration.appliesRTL = false + storage = .wiggle(configuration) + } +} + +extension _SymbolEffect.Storage { + fileprivate var trigger: _SymbolEffect.Trigger? { + get { + switch self { + case let .pulse(configuration): configuration.trigger + case let .breathe(configuration): configuration.trigger + case let .bounce(configuration): configuration.trigger + case let .wiggle(configuration): configuration.trigger + case let .rotate(configuration): configuration.trigger + case let .variableColor(configuration): configuration.trigger + case .scale, .hidden, .none: nil + } + } + set { + switch self { + case var .pulse(configuration): + configuration.trigger = newValue + self = .pulse(configuration) + case var .breathe(configuration): + configuration.trigger = newValue + self = .breathe(configuration) + case var .bounce(configuration): + configuration.trigger = newValue + self = .bounce(configuration) + case var .wiggle(configuration): + configuration.trigger = newValue + self = .wiggle(configuration) + case var .rotate(configuration): + configuration.trigger = newValue + self = .rotate(configuration) + case var .variableColor(configuration): + configuration.trigger = newValue + self = .variableColor(configuration) + case .scale, .hidden, .none: + break + } + } + } +} + +// MARK: - EnvironmentValues + symbolEffects + +extension EnvironmentValues { + private struct SymbolEffectsKey: EnvironmentKey { + static let defaultValue: [_SymbolEffect.Identified] = [] + } + + package var symbolEffects: [_SymbolEffect.Identified] { + get { self[SymbolEffectsKey.self] } + set { self[SymbolEffectsKey.self] = newValue } + } + + package mutating func appendSymbolEffect( + _ effect: _SymbolEffect, + for identifier: Int + ) { + var effect = effect + effect.applyLayoutDirection(from: self) + symbolEffects.append(.init(identifier: identifier, effect: effect)) + } +} + +// MARK: - GraphicsImage + updateSymbolEffects + +extension GraphicsImage { + mutating func updateSymbolEffects( + _ phase: inout _SymbolEffect.Phase, + environment: EnvironmentValues, + transaction: Attribute, + animationsDisabled: Bool + ) -> ORBSymbolAnimator? { + guard case var .vectorGlyph(glyph) = contents else { + return nil + } + let animator = glyph.animator + animator.updateSymbolEffects( + &phase, + effects: environment.symbolEffects, + transaction: transaction, + animationsDisabled: animationsDisabled + ) + glyph.animatorVersion = animator.version + glyph.allowsContentTransitions = glyph.allowsContentTransitions && !phase.hidden.isHidden + contents = .vectorGlyph(glyph) + return animator + } +} + +// MARK: - ORBSymbolAnimator + SymbolEffect + +extension ORBSymbolAnimator { + fileprivate func updateSymbolEffects( + _ phase: inout _SymbolEffect.Phase, + effects: [_SymbolEffect.Identified], + transaction: Attribute, + animationsDisabled: Bool + ) { + guard !phase.added.isEmpty + || phase.scale.level != .normal + || phase.hidden.isHidden + || !effects.isEmpty + else { + return + } + let currentTransaction = Graph.withoutUpdate { transaction.value } + let disablesAnimations = currentTransaction.disablesAnimations + let animationListener = currentTransaction.animationListener + let logicalListener = currentTransaction.animationLogicalListener + + var matched = BitVector(count: phase.added.count) + var unmatchedCount = phase.added.count + var added: [_SymbolEffect.Added] = [] + var scale = phase.scale + scale.level = .normal + var scaleOptions = phase.scaleOptions + var hidden = phase.hidden + hidden.isHidden = false + var hiddenOptions = phase.hiddenOptions + var priority: Float = 0 + + for identified in effects { + let storage = identified.effect.storage + let options = identified.effect.options + switch storage { + case let .scale(configuration): + scale = configuration + scaleOptions = options + continue + case let .hidden(configuration): + hidden = configuration + hiddenOptions = options + continue + default: + break + } + let trigger = storage.trigger + var isNew = true + if let index = phase.added.firstIndex(where: { + $0.identifier == identified.identifier && $0.serial == identified.serial + }) { + var old = phase.added[index] + matched[index] = true + unmatchedCount -= 1 + let restarts: Bool + if let trigger { + restarts = storage.hasSameCase(as: old.effect) + && trigger.activates(after: old.effect.trigger) + } else { + restarts = storage != old.effect + } + if !restarts { + if old.animationID != 0 { + if old.priority != priority { + old.priority = priority + setPriority(priority, ofAnimationWithID: old.animationID) + } + priority += 1 + } + old.effect.trigger = trigger + added.append(old) + continue + } + if old.animationID != 0 { + cancelAnimation(withID: old.animationID) + } + isNew = false + } + + var animationID: UInt32 = 0 + var animationPriority: Float = .nan + if !animationsDisabled, trigger == nil || (!isNew && !disablesAnimations) { + var animationOptions: [ORBSymbolAnimation.OptionKey: Any] = [:] + let animation: ORBSymbolAnimation? + switch storage { + case let .pulse(configuration): + animationOptions[.byLayer] = configuration.layered + animation = .pulse + case let .breathe(configuration): + animationOptions[.byLayer] = configuration.layered + animationOptions[.breatheOptions] = configuration.flags.rawValue + animation = .breathe + case let .bounce(configuration): + animationOptions[.bounceOptions] = configuration.flags.rawValue + animationOptions[.byLayer] = configuration.layered + animation = .bounce + case let .wiggle(configuration): + animationOptions[.wiggleOptions] = configuration.flags.rawValue + if let angle = configuration.angle { + animationOptions[.wiggleAngle] = angle.radians + } + animationOptions[.byLayer] = configuration.layered + animation = .wiggle + case let .rotate(configuration): + animationOptions[.rotateOptions] = configuration.flags.rawValue + animationOptions[.byLayer] = configuration.layered + animation = .rotate + case let .variableColor(configuration): + animationOptions[.variableColorOptions] = configuration.flags.rawValue + animation = .variableColor + case .scale, .hidden, .none: + animation = nil + } + if let animation { + animationOptions[.speed] = options.speed + animationOptions[.repeatCount] = options.repeatCount ?? (trigger == nil ? Float.infinity : 1) + if let repeatDelay = options.repeatDelay { + animationOptions[.repeatDelay] = repeatDelay + } + animationOptions[.priority] = priority + animationID = addAnimation( + animation, + options: animationOptions, + animationListener: animationListener, + logicalListener: logicalListener + ) + if animationID != 0 { + animationPriority = priority + priority += 1 + } + } + } + added.append( + .init( + identifier: identified.identifier, + serial: identified.serial, + effect: storage, + animationID: animationID, + priority: animationPriority + ) + ) + } + + if unmatchedCount > 0 { + for index in phase.added.indices where !matched[index] { + let animationID = phase.added[index].animationID + if animationID != 0 { + cancelAnimation(withID: animationID) + } + } + } + + if scale.level != phase.scale.level || hidden.isHidden != phase.hidden.isHidden { + if !animationsDisabled, !disablesAnimations, !phase.isFirstUpdate { + if scale != phase.scale, + scale.level != .normal || phase.scale.level != .normal { + addAnimation( + .scale, + options: [ + .byLayer: scale.layered, + .speed: scaleOptions.speed, + ], + animationListener: animationListener, + logicalListener: logicalListener + ) + } + if hidden.isHidden != phase.hidden.isHidden { + addAnimation( + hidden.isHidden ? .disappear : .appear, + options: [ + .byLayer: hidden.layered, + .appearDisappearOptions: hidden.flags.rawValue, + .speed: hiddenOptions.speed, + ], + animationListener: animationListener, + logicalListener: logicalListener + ) + } + } + scaleLevel = scale.level + isHidden = hidden.isHidden + } + + phase.added = added + phase.scale = scale + phase.scaleOptions = scaleOptions + phase.hidden = hidden + phase.hiddenOptions = hiddenOptions + phase.isFirstUpdate = false + } + + @discardableResult + func addAnimation( + _ animation: ORBSymbolAnimation, + options: consuming [ORBSymbolAnimation.OptionKey: Any], + animationListener: AnimationListener?, + logicalListener: AnimationListener? + ) -> UInt32 { + var options = options + if animationListener != nil || logicalListener != nil { + #if canImport(ObjectiveC) + let completion: @convention(block) (Bool, UInt32) -> Void = { _, _ in + Update.enqueueAction(reason: nil) { + animationListener?.animationWasRemoved() + logicalListener?.animationWasRemoved() + } + } + #else + // On Linux, Swift 6.3.3 can call blocks but crashes when + // options.map copies a block stored as Any. Use a Swift closure. + let completion: (Bool, UInt32) -> Void = { _, _ in + Update.enqueueAction(reason: nil) { + animationListener?.animationWasRemoved() + logicalListener?.animationWasRemoved() + } + } + #endif + options[.onCompletion] = completion + } + let animationOptions = Dictionary( + uniqueKeysWithValues: options.map { ($0.key.rawValue, $0.value) } + ) + let animationID = addAnimation(animation, options: animationOptions) + if animationID != 0 { + animationListener?.animationWasAdded() + logicalListener?.animationWasAdded() + } + return animationID + } +} + +extension _SymbolEffect.Storage { + fileprivate func hasSameCase(as other: _SymbolEffect.Storage) -> Bool { + switch (self, other) { + case (.pulse, .pulse), (.breathe, .breathe), (.bounce, .bounce), + (.wiggle, .wiggle), (.rotate, .rotate), (.variableColor, .variableColor), + (.scale, .scale), (.hidden, .hidden), (.none, .none): + true + default: + false + } + } +} + +// MARK: - _SymbolEffect + ProtobufMessage + +extension _SymbolEffect: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + try encoder.messageField(1, storage) + try encoder.messageField(2, options, defaultValue: .init()) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var storage: Storage? + var options = Options() + while let field = try decoder.nextField() { + switch field.tag { + case 1: storage = try decoder.messageField(field) + case 2: options = try decoder.messageField(field) + default: try decoder.skipField(field) + } + } + guard let storage else { + throw ProtobufDecoder.DecodingError.failed + } + self.init(storage, options: options) + } +} + +// MARK: - _SymbolEffect.Storage + ProtobufMessage + +extension _SymbolEffect.Storage: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + switch self { + case let .pulse(configuration): + try encoder.messageField(1, configuration) + case let .breathe(configuration): + try encoder.messageField(8, configuration) + case let .bounce(configuration): + try encoder.messageField(2, configuration) + case let .wiggle(configuration): + try encoder.messageField(3, configuration) + case let .rotate(configuration): + try encoder.messageField(4, configuration) + case let .variableColor(configuration): + try encoder.messageField(5, configuration) + case let .scale(configuration): + try encoder.messageField(6, configuration) + case let .hidden(configuration): + try encoder.messageField(7, configuration) + case .none: + break + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var storage: Self = .none + while let field = try decoder.nextField() { + switch field.tag { + case 1: storage = .pulse(try decoder.messageField(field)) + case 2: storage = .bounce(try decoder.messageField(field)) + case 3: storage = .wiggle(try decoder.messageField(field)) + case 4: storage = .rotate(try decoder.messageField(field)) + case 5: storage = .variableColor(try decoder.messageField(field)) + case 6: storage = .scale(try decoder.messageField(field)) + case 7: storage = .hidden(try decoder.messageField(field)) + case 8: storage = .breathe(try decoder.messageField(field)) + default: try decoder.skipField(field) + } + } + self = storage + } +} + +// MARK: - _SymbolEffect.Options + ProtobufMessage + +extension _SymbolEffect.Options: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.floatField(1, speed, defaultValue: 1) + if let repeatCount { + encoder.floatField(2, repeatCount) + } + if let repeatDelay { + encoder.floatField(3, repeatDelay) + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var options = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 1: options.speed = try decoder.floatField(field) + case 2: options.repeatCount = try decoder.floatField(field) + case 3: options.repeatDelay = try decoder.floatField(field) + default: try decoder.skipField(field) + } + } + self = options + } +} + +// MARK: - _SymbolEffect.Trigger + ProtobufMessage + +extension _SymbolEffect.Trigger: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + switch self { + case let .value(value): + try encoder.messageField(1, value.hash ?? .random()) + case let .condition(condition): + encoder.boolField(2, condition) + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var trigger: Self = .condition(false) + while let field = try decoder.nextField() { + switch field.tag { + case 1: + let hash: StrongHash = try decoder.messageField(field) + trigger = .value(AnyEquatable(hash: hash)) + case 2: + trigger = .condition(try decoder.boolField(field)) + default: + try decoder.skipField(field) + } + } + self = trigger + } +} + +// MARK: - _SymbolEffect.PulseConfiguration + ProtobufMessage + +extension _SymbolEffect.PulseConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(3, trigger) + } + encoder.boolField(2, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 2: configuration.layered = try decoder.boolField(field) + case 3: configuration.trigger = try decoder.messageField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.BreatheConfiguration + ProtobufMessage + +extension _SymbolEffect.BreatheConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(1, trigger) + } + encoder.uintField(2, UInt(flags.rawValue)) + encoder.boolField(3, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 1: configuration.trigger = try decoder.messageField(field) + case 2: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 3: configuration.layered = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.BounceConfiguration + ProtobufMessage + +extension _SymbolEffect.BounceConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(4, trigger) + } + encoder.uintField(2, UInt(flags.rawValue)) + encoder.boolField(3, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 2: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 3: configuration.layered = try decoder.boolField(field) + case 4: configuration.trigger = try decoder.messageField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.WiggleConfiguration + ProtobufMessage + +extension _SymbolEffect.WiggleConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(5, trigger) + } + if let angle { + encoder.doubleField(2, angle.radians) + } + encoder.uintField(3, UInt(flags.rawValue)) + encoder.boolField(4, layered, defaultValue: true) + encoder.boolField(6, appliesRTL) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 2: configuration.angle = .init(radians: try decoder.doubleField(field)) + case 3: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 4: configuration.layered = try decoder.boolField(field) + case 5: configuration.trigger = try decoder.messageField(field) + case 6: configuration.appliesRTL = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.RotateConfiguration + ProtobufMessage + +extension _SymbolEffect.RotateConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(4, trigger) + } + encoder.uintField(2, UInt(flags.rawValue)) + encoder.boolField(3, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 2: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 3: configuration.layered = try decoder.boolField(field) + case 4: configuration.trigger = try decoder.messageField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.VariableColorConfiguration + ProtobufMessage + +extension _SymbolEffect.VariableColorConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let trigger { + try encoder.messageField(3, trigger) + } + encoder.uintField(2, UInt(flags.rawValue)) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 2: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 3: configuration.trigger = try decoder.messageField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - ORBSymbolScaleLevel + ProtobufEnum + +extension ORBSymbolScaleLevel: ProtobufEnum { + package var protobufValue: UInt { + UInt(rawValue) + } + + package init?(protobufValue value: UInt) { + guard let rawValue = Int32(exactly: value) else { + return nil + } + self.init(rawValue: rawValue) + } +} + +// MARK: - _SymbolEffect.ScaleConfiguration + ProtobufMessage + +extension _SymbolEffect.ScaleConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.enumField(1, level) + encoder.boolField(2, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 1: configuration.level = try decoder.enumField(field) ?? .normal + case 2: configuration.layered = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.HiddenConfiguration + ProtobufMessage + +extension _SymbolEffect.HiddenConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.boolField(1, isHidden) + encoder.uintField(2, UInt(flags.rawValue)) + encoder.boolField(3, layered, defaultValue: true) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 1: configuration.isHidden = try decoder.boolField(field) + case 2: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 3: configuration.layered = try decoder.boolField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect.ReplaceConfiguration + ProtobufMessage + +extension _SymbolEffect.ReplaceConfiguration: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.uintField(1, UInt(flags.rawValue)) + encoder.boolField(2, layered, defaultValue: true) + encoder.floatField(3, speed, defaultValue: 1) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var configuration = Self() + while let field = try decoder.nextField() { + switch field.tag { + case 1: configuration.flags = .init(rawValue: try decoder.uint32Field(field)) + case 2: configuration.layered = try decoder.boolField(field) + case 3: configuration.speed = try decoder.floatField(field) + default: try decoder.skipField(field) + } + } + self = configuration + } +} + +// MARK: - _SymbolEffect + isIndefinite + +extension _SymbolEffect { + package var isIndefinite: Bool { + if let repeatCount = options.repeatCount { + return !repeatCount.isFinite + } + switch storage { + case .scale, .hidden, .none: + return true + default: + return storage.trigger == nil + } + } +} diff --git a/Tests/OpenSwiftUICoreTests/Render/SymbolEffectTests.swift b/Tests/OpenSwiftUICoreTests/Render/SymbolEffectTests.swift new file mode 100644 index 000000000..d981ef416 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Render/SymbolEffectTests.swift @@ -0,0 +1,100 @@ +// +// SymbolEffectTests.swift +// OpenSwiftUICoreTests + +import Foundation +@_spi(Private) @testable import OpenSwiftUICore +import Testing + +struct SymbolEffectTests { + typealias Trigger = _SymbolEffect.Trigger + + @Test(arguments: [ + (Trigger.condition(true), Trigger?.none, false), + (Trigger.condition(true), Trigger?.some(.condition(false)), true), + (Trigger.condition(false), Trigger?.some(.condition(true)), false), + (Trigger.condition(true), Trigger?.some(.condition(true)), false), + (Trigger.value(AnyEquatable(2)), Trigger?.some(.value(AnyEquatable(1))), true), + (Trigger.value(AnyEquatable(1)), Trigger?.some(.value(AnyEquatable(1))), false), + (Trigger.value(AnyEquatable(1)), Trigger?.some(.condition(false)), false), + ]) + func triggerActivates(trigger: Trigger, oldTrigger: Trigger?, expected: Bool) { + #expect(trigger.activates(after: oldTrigger) == expected) + } + + @Test + func isIndefinite() { + var pulse = _SymbolEffect.PulseConfiguration() + #expect(_SymbolEffect(.pulse(pulse)).isIndefinite == true) + pulse.trigger = .condition(true) + #expect(_SymbolEffect(.pulse(pulse)).isIndefinite == false) + #expect(_SymbolEffect(.scale(.init())).isIndefinite == true) + + var options = _SymbolEffect.Options() + options.repeatCount = 2 + #expect(_SymbolEffect(.pulse(.init()), options: options).isIndefinite == false) + options.repeatCount = .infinity + #expect(_SymbolEffect(.pulse(pulse), options: options).isIndefinite == true) + } + + @Test(arguments: [ + (UInt32(0x01), UInt32(0x21)), + (UInt32(0x11), UInt32(0x21)), + (UInt32(0x21), UInt32(0x11)), + (UInt32(0x31), UInt32(0x01)), + ]) + func appendSymbolEffectAppliesRightToLeft(flags: UInt32, expected: UInt32) throws { + var wiggle = _SymbolEffect.WiggleConfiguration() + wiggle.flags = .init(rawValue: flags) + wiggle.appliesRTL = true + var environment = EnvironmentValues() + environment.layoutDirection = .rightToLeft + environment.appendSymbolEffect(_SymbolEffect(.wiggle(wiggle)), for: 7) + + let identified = try #require(environment.symbolEffects.first) + #expect(identified.identifier == 7) + #expect(identified.serial == 0) + guard case let .wiggle(result) = identified.effect.storage else { + Issue.record("Expected a wiggle effect") + return + } + #expect(result.flags.rawValue == expected) + #expect(result.appliesRTL == false) + } + + @Test + func appendSymbolEffectKeepsLeftToRight() throws { + var wiggle = _SymbolEffect.WiggleConfiguration() + wiggle.flags = .init(rawValue: 0x11) + wiggle.appliesRTL = true + var environment = EnvironmentValues() + environment.layoutDirection = .leftToRight + environment.appendSymbolEffect(_SymbolEffect(.wiggle(wiggle)), for: 1) + + let identified = try #require(environment.symbolEffects.first) + #expect(identified.effect == _SymbolEffect(.wiggle(wiggle))) + } + + @Test + func protobufEncoding() throws { + var bounce = _SymbolEffect.BounceConfiguration() + bounce.flags = .up + bounce.layered = false + try expectEncoding(_SymbolEffect(.bounce(bounce)), hex: "0a06120410011800") + + var pulse = _SymbolEffect.PulseConfiguration() + pulse.trigger = .condition(true) + try expectEncoding(_SymbolEffect(.pulse(pulse)), hex: "0a060a041a021001") + + var options = _SymbolEffect.Options() + options.speed = 2 + try expectEncoding(_SymbolEffect(.pulse(.init()), options: options), hex: "0a020a0012050d00000040") + } + + private func expectEncoding(_ effect: _SymbolEffect, hex: String) throws { + let data = try ProtobufEncoder.encoding(effect) + #expect(data.map { String(format: "%02x", $0) }.joined() == hex) + var decoder = ProtobufDecoder(data) + #expect(try _SymbolEffect(from: &decoder) == effect) + } +} From 57f7626ca4f6001de1a6e11b88cc2ea039ceb4fe Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 24 Oct 2026 15:23:28 +0800 Subject: [PATCH 4/5] feat: implement SymbolEffect --- .../Render/SymbolEffect/SymbolEffect.swift | 841 ++++++++++++++++++ .../OpenSwiftUI/View/Image/SymbolEffect.swift | 128 --- .../Transition/ContentTransition.swift | 1 + .../OpenSwiftUICore/View/Image/Image.swift | 44 +- 4 files changed, 845 insertions(+), 169 deletions(-) create mode 100644 Sources/OpenSwiftUI/Render/SymbolEffect/SymbolEffect.swift delete mode 100644 Sources/OpenSwiftUI/View/Image/SymbolEffect.swift diff --git a/Sources/OpenSwiftUI/Render/SymbolEffect/SymbolEffect.swift b/Sources/OpenSwiftUI/Render/SymbolEffect/SymbolEffect.swift new file mode 100644 index 000000000..c4bbd83f2 --- /dev/null +++ b/Sources/OpenSwiftUI/Render/SymbolEffect/SymbolEffect.swift @@ -0,0 +1,841 @@ +// +// SymbolEffect.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: C350E1C6FA5CB264BFEA1DF73B9E8FA7 (SwiftUI) + +import OpenAttributeGraphShims +@_spi(Private) public import OpenSwiftUICore + +// MARK: - SymbolEffect + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +public struct SymbolEffect: Equatable { + package var base: _SymbolEffect + + package init(base: _SymbolEffect) { + self.base = base + } + + @available(OpenSwiftUI_v6_0, *) + public func speed(_ speed: Double) -> SymbolEffect { + var effect = self + effect.base.options.speed = Float(speed) + return effect + } + + @available(OpenSwiftUI_v6_0, *) + public func repeatCount(_ value: Double?) -> SymbolEffect { + var effect = self + effect.base.options.repeatCount = value.map { Float($0) } + return effect + } + + @available(OpenSwiftUI_v6_0, *) + public func repeatDelay(_ value: Double?) -> SymbolEffect { + var effect = self + effect.base.options.repeatDelay = value.map { Float($0) } + return effect + } + + // MARK: - Pulse + + public struct PulseConfiguration: Equatable { + package var base: _SymbolEffect.PulseConfiguration + + package init(base: _SymbolEffect.PulseConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.PulseConfiguration { + .init(base: .init()) + } + + @available(*, deprecated, message: "no longer supported") + public static var slow: SymbolEffect.PulseConfiguration { + .init(base: .init()) + } + + @available(*, deprecated, message: "no longer supported") + public static var fast: SymbolEffect.PulseConfiguration { + .init(base: .init()) + } + + public static var byLayer: SymbolEffect.PulseConfiguration { + .init(base: .init()) + } + + public static var wholeSymbol: SymbolEffect.PulseConfiguration { + var base = _SymbolEffect.PulseConfiguration() + base.layered = false + return .init(base: base) + } + + public var byLayer: SymbolEffect.PulseConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.PulseConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + } + + public static func pulse(_ config: SymbolEffect.PulseConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.pulse(config.base))) + } + + public static var pulse: SymbolEffect { + SymbolEffect(base: _SymbolEffect(.pulse(.init()))) + } + + // MARK: - Breathe + + @available(OpenSwiftUI_v6_0, *) + public struct BreatheConfiguration: Equatable { + package var base: _SymbolEffect.BreatheConfiguration + + package init(base: _SymbolEffect.BreatheConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.BreatheConfiguration { + .init(base: .init()) + } + + public var onlyScale: SymbolEffect.BreatheConfiguration { + var base = _SymbolEffect.BreatheConfiguration() + base.flags.insert(.onlyScale) + return .init(base: base) + } + + public static var wholeSymbol: SymbolEffect.BreatheConfiguration { + var base = _SymbolEffect.BreatheConfiguration() + base.layered = false + return .init(base: base) + } + + public var byLayer: SymbolEffect.BreatheConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.BreatheConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + } + + @available(OpenSwiftUI_v6_0, *) + public static func breathe(_ config: SymbolEffect.BreatheConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.breathe(config.base))) + } + + @available(OpenSwiftUI_v6_0, *) + public static var breathe: SymbolEffect { + SymbolEffect(base: _SymbolEffect(.breathe(.init()))) + } + + // MARK: - Bounce + + public struct BounceConfiguration: Equatable { + package var base: _SymbolEffect.BounceConfiguration + + package init(base: _SymbolEffect.BounceConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.BounceConfiguration { + .init(base: .init()) + } + + public static var up: SymbolEffect.BounceConfiguration { + var base = _SymbolEffect.BounceConfiguration() + base.flags = .up + return .init(base: base) + } + + public static var down: SymbolEffect.BounceConfiguration { + var base = _SymbolEffect.BounceConfiguration() + base.flags = .down + return .init(base: base) + } + + public static var byLayer: SymbolEffect.BounceConfiguration { + .init(base: .init()) + } + + public static var wholeSymbol: SymbolEffect.BounceConfiguration { + var base = _SymbolEffect.BounceConfiguration() + base.layered = false + return .init(base: base) + } + + public var byLayer: SymbolEffect.BounceConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.BounceConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + } + + public static func bounce( + _ config: SymbolEffect.BounceConfiguration = .default, + value: T + ) -> SymbolEffect where T: Equatable { + var base = config.base + base.trigger = .value(AnyEquatable(value)) + return SymbolEffect(base: _SymbolEffect(.bounce(base))) + } + + @available(OpenSwiftUI_v6_0, *) + public static func bounce(_ config: SymbolEffect.BounceConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.bounce(config.base))) + } + + // MARK: - Wiggle + + @available(OpenSwiftUI_v6_0, *) + public struct WiggleConfiguration: Equatable { + package var base: _SymbolEffect.WiggleConfiguration + + package init(base: _SymbolEffect.WiggleConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.WiggleConfiguration { + .init(base: .init()) + } + + public static func linear(angle: Angle) -> SymbolEffect.WiggleConfiguration { + var base = _SymbolEffect.WiggleConfiguration() + base.angle = angle + return .init(base: base) + } + + public static var rotational: SymbolEffect.WiggleConfiguration { + var base = _SymbolEffect.WiggleConfiguration() + base.flags = .rotational + return .init(base: base) + } + + public func continuous(_ enabled: Bool = true) -> SymbolEffect.WiggleConfiguration { + var configuration = self + if enabled { + configuration.base.flags.insert(.continuous) + } else { + configuration.base.flags.remove(.continuous) + } + return configuration + } + + public func byLayer(_ enabled: Bool) -> SymbolEffect.WiggleConfiguration { + var configuration = self + configuration.base.layered = enabled + return configuration + } + } + + @available(OpenSwiftUI_v6_0, *) + public static func wiggle(_ config: SymbolEffect.WiggleConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.wiggle(config.base))) + } + + @available(OpenSwiftUI_v6_0, *) + public static func wiggle( + _ config: SymbolEffect.WiggleConfiguration = .default, + value: T + ) -> SymbolEffect where T: Equatable { + var base = config.base + base.trigger = .value(AnyEquatable(value)) + return SymbolEffect(base: _SymbolEffect(.wiggle(base))) + } + + // MARK: - Rotate + + @available(OpenSwiftUI_v6_0, *) + public struct RotateConfiguration: Equatable { + package var base: _SymbolEffect.RotateConfiguration + + package init(base: _SymbolEffect.RotateConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.RotateConfiguration { + .init(base: .init()) + } + + public static var clockwise: SymbolEffect.RotateConfiguration { + var base = _SymbolEffect.RotateConfiguration() + base.flags = .clockwise + return .init(base: base) + } + + public static var counterClockwise: SymbolEffect.RotateConfiguration { + var base = _SymbolEffect.RotateConfiguration() + base.flags = .counterClockwise + return .init(base: base) + } + + public func continuous(_ enabled: Bool = true) -> SymbolEffect.RotateConfiguration { + var configuration = self + if enabled { + configuration.base.flags.insert(.continuous) + } else { + configuration.base.flags.remove(.continuous) + } + return configuration + } + + public func byLayer(_ enabled: Bool) -> SymbolEffect.RotateConfiguration { + var configuration = self + configuration.base.layered = enabled + return configuration + } + } + + @available(OpenSwiftUI_v6_0, *) + public static func rotate(_ config: SymbolEffect.RotateConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.rotate(config.base))) + } + + @available(OpenSwiftUI_v6_0, *) + public static func rotate( + _ config: SymbolEffect.RotateConfiguration = .default, + value: T + ) -> SymbolEffect where T: Equatable { + var base = config.base + base.trigger = .value(AnyEquatable(value)) + return SymbolEffect(base: _SymbolEffect(.rotate(base))) + } + + // MARK: - VariableColor + + public struct VariableColorConfiguration: Equatable { + package var base: _SymbolEffect.VariableColorConfiguration + + package init(base: _SymbolEffect.VariableColorConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.VariableColorConfiguration { + .init(base: .init()) + } + + public static var cumulative: SymbolEffect.VariableColorConfiguration { + .init(base: .init()) + } + + public static var sequential: SymbolEffect.VariableColorConfiguration { + var base = _SymbolEffect.VariableColorConfiguration() + base.flags = .iterative + return .init(base: base) + } + + public var reversing: SymbolEffect.VariableColorConfiguration { + var configuration = self + configuration.base.flags.insert(.reversing) + return configuration + } + + public var nonReversing: SymbolEffect.VariableColorConfiguration { + var configuration = self + configuration.base.flags.remove(.reversing) + return configuration + } + + public var hideOn: SymbolEffect.VariableColorConfiguration { + var configuration = self + configuration.base.flags.insert(.hideInactiveLayers) + return configuration + } + + public var hideOff: SymbolEffect.VariableColorConfiguration { + var configuration = self + configuration.base.flags.remove(.hideInactiveLayers) + return configuration + } + } + + public static func variableColor(_ config: SymbolEffect.VariableColorConfiguration = .default) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.variableColor(config.base))) + } + + // MARK: - Scale + + public struct ScaleConfiguration: Equatable { + package var base: _SymbolEffect.ScaleConfiguration + + package init(base: _SymbolEffect.ScaleConfiguration) { + self.base = base + } + + public static var none: SymbolEffect.ScaleConfiguration { + .init(base: .init()) + } + + public static var up: SymbolEffect.ScaleConfiguration { + var base = _SymbolEffect.ScaleConfiguration() + base.level = .up + return .init(base: base) + } + + public static var down: SymbolEffect.ScaleConfiguration { + var base = _SymbolEffect.ScaleConfiguration() + base.level = .down + return .init(base: base) + } + + public var byLayer: SymbolEffect.ScaleConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.ScaleConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + } + + public static func scale(_ config: SymbolEffect.ScaleConfiguration) -> SymbolEffect { + SymbolEffect(base: _SymbolEffect(.scale(config.base))) + } + + @_alwaysEmitIntoClient + public static var scaleUp: SymbolEffect { + scale(.up) + } + + @_alwaysEmitIntoClient + public static var scaleDown: SymbolEffect { + scale(.down) + } + + // MARK: - Hidden + + public struct HiddenConfiguration: Equatable { + package var base: _SymbolEffect.HiddenConfiguration + + package init(base: _SymbolEffect.HiddenConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.HiddenConfiguration { + .init(base: .init()) + } + + public static var down: SymbolEffect.HiddenConfiguration { + var base = _SymbolEffect.HiddenConfiguration() + base.flags = .down + return .init(base: base) + } + + public static var up: SymbolEffect.HiddenConfiguration { + var base = _SymbolEffect.HiddenConfiguration() + base.flags = .up + return .init(base: base) + } + + public static var byLayer: SymbolEffect.HiddenConfiguration { + .init(base: .init()) + } + + public static var wholeSymbol: SymbolEffect.HiddenConfiguration { + var base = _SymbolEffect.HiddenConfiguration() + base.layered = false + return .init(base: base) + } + + public var byLayer: SymbolEffect.HiddenConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.HiddenConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + } + + public static func hidden( + _ isHidden: Bool = true, + _ config: SymbolEffect.HiddenConfiguration = .default + ) -> SymbolEffect { + var base = config.base + base.isHidden = isHidden + return SymbolEffect(base: _SymbolEffect(.hidden(base))) + } + + // MARK: - Replace + + public struct ReplaceConfiguration: Equatable { + package var base: _SymbolEffect.ReplaceConfiguration + + package init(base: _SymbolEffect.ReplaceConfiguration) { + self.base = base + } + + public static var `default`: SymbolEffect.ReplaceConfiguration { + .init(base: .init()) + } + + public static var up: SymbolEffect.ReplaceConfiguration { + var base = _SymbolEffect.ReplaceConfiguration() + base.flags = .up + return .init(base: base) + } + + public static var down: SymbolEffect.ReplaceConfiguration { + var base = _SymbolEffect.ReplaceConfiguration() + base.flags = .down + return .init(base: base) + } + + public static var offUp: SymbolEffect.ReplaceConfiguration { + var base = _SymbolEffect.ReplaceConfiguration() + base.flags = .offUp + return .init(base: base) + } + + public static var byLayer: SymbolEffect.ReplaceConfiguration { + .init(base: .init()) + } + + public static var wholeSymbol: SymbolEffect.ReplaceConfiguration { + var base = _SymbolEffect.ReplaceConfiguration() + base.layered = false + return .init(base: base) + } + + public var byLayer: SymbolEffect.ReplaceConfiguration { + var configuration = self + configuration.base.layered = true + return configuration + } + + public var wholeSymbol: SymbolEffect.ReplaceConfiguration { + var configuration = self + configuration.base.layered = false + return configuration + } + + @available(OpenSwiftUI_v6_0, *) + public var noSlashes: SymbolEffect.ReplaceConfiguration { + var configuration = self + configuration.base.flags.insert(.noSlashes) + return configuration + } + + @available(OpenSwiftUI_v6_0, *) + public var noBadges: SymbolEffect.ReplaceConfiguration { + var configuration = self + configuration.base.flags.insert(.noBadges) + return configuration + } + } +} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.PulseConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.BreatheConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.BounceConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.WiggleConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.RotateConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.VariableColorConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.ScaleConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.HiddenConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.ReplaceConfiguration: Sendable {} + +// MARK: - View + symbolEffect + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension View { + @_alwaysEmitIntoClient + nonisolated public func symbolEffect(_ effect: SymbolEffect?) -> some View { + modifier(_SymbolEffectModifier(effect: effect)) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v6_0, *) +extension View { + @_alwaysEmitIntoClient + nonisolated public func symbolEffects(_ effects: [SymbolEffect]) -> some View { + modifier(_SymbolEffectsModifier(effects: effects)) + } +} + +@available(OpenSwiftUI_v5_0, *) +extension View { + @_alwaysEmitIntoClient + nonisolated public func symbolEffectsRemoved(_ isEnabled: Bool = true) -> some View { + modifier(_SymbolEffectsRemovedModifier(isEnabled: isEnabled)) + } +} + +// MARK: - _SymbolEffectModifier + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +@frozen +public struct _SymbolEffectModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { + @usableFromInline + package var effect: SymbolEffect? + + @usableFromInline + package init(effect: SymbolEffect?) { + self.effect = effect + } + + public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { + inputs.environment = Attribute( + EffectEnvironment( + modifier: modifier.value, + environment: inputs.environment, + identifier: Int(makeUniqueID()) + ) + ) + } + + private struct EffectEnvironment: Rule, AsyncAttribute { + @Attribute var modifier: _SymbolEffectModifier + @Attribute var environment: EnvironmentValues + let identifier: Int + + var value: EnvironmentValues { + var environment = environment + if let effect = modifier.effect { + environment.appendSymbolEffect(effect.base, for: identifier) + } + return environment + } + } +} + +@_spi(Private) +@available(*, unavailable) +extension _SymbolEffectModifier: Sendable {} + +// MARK: - _SymbolEffectsModifier + +@_spi(Private) +@available(OpenSwiftUI_v6_0, *) +@frozen +public struct _SymbolEffectsModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { + @usableFromInline + var effects: [SymbolEffect] + + @_alwaysEmitIntoClient + init(effects: [SymbolEffect]) { + self.effects = effects + } + + public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { + inputs.environment = Attribute( + EffectsEnvironment( + modifier: modifier.value, + environment: inputs.environment, + identifier: Int(makeUniqueID()) + ) + ) + } + + private struct EffectsEnvironment: Rule, AsyncAttribute { + @Attribute var modifier: _SymbolEffectsModifier + @Attribute var environment: EnvironmentValues + let identifier: Int + + var value: EnvironmentValues { + var environment = environment + let effects = modifier.effects + guard !effects.isEmpty else { + return environment + } + var symbolEffects = environment.symbolEffects + for (offset, effect) in effects.enumerated() { + symbolEffects.append( + .init(identifier: identifier, serial: offset, effect: effect.base) + ) + } + environment.symbolEffects = symbolEffects + return environment + } + } +} + +@_spi(Private) +@available(*, unavailable) +extension _SymbolEffectsModifier: Sendable {} + +// MARK: - _SymbolEffectsRemovedModifier + +@available(OpenSwiftUI_v5_0, *) +@frozen +public struct _SymbolEffectsRemovedModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { + @usableFromInline + var isEnabled: Bool + + @_alwaysEmitIntoClient + init(isEnabled: Bool) { + self.isEnabled = isEnabled + } + + public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { + inputs.environment = Attribute( + EffectEnvironment( + modifier: modifier.value, + environment: inputs.environment + ) + ) + } + + private struct EffectEnvironment: Rule, AsyncAttribute { + @Attribute var modifier: _SymbolEffectsRemovedModifier + @Attribute var environment: EnvironmentValues + + var value: EnvironmentValues { + var environment = environment + if modifier.isEnabled { + environment.symbolEffects = [] + } + return environment + } + } +} + +@available(*, unavailable) +extension _SymbolEffectsRemovedModifier: Sendable {} + +// MARK: - SymbolEffect.HiddenTransition + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension Transition where Self == SymbolEffect.HiddenTransition { + @_alwaysEmitIntoClient + @MainActor + @preconcurrency + public static func symbolHidden(_ config: SymbolEffect.HiddenConfiguration = .default) -> SymbolEffect.HiddenTransition { + .init(config: config) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension SymbolEffect { + @MainActor + @preconcurrency + public struct HiddenTransition: Transition { + public var config: SymbolEffect.HiddenConfiguration + + public init(config: SymbolEffect.HiddenConfiguration) { + self.config = config + } + + public func body(content: Content, phase: TransitionPhase) -> some View { + var config = config + config.base.isHidden = !phase.isIdentity + let effect = SymbolEffect(base: _SymbolEffect(.hidden(config.base))) + return content.modifier(_SymbolEffectModifier(effect: effect)) + } + + public static let properties = TransitionProperties(hasMotion: true) + + public func _makeContentTransition(transition: inout _Transition_ContentTransition) { + switch transition.operation { + case .effects: + let effect = ContentTransition.Effect( + type: ContentTransition.EffectType( + type: .symbolAppearDisappear, + arg0: .int(config.base.transitionFlags.rawValue), + arg1: .float(1) + ) + ) + transition.result = .effects([effect]) + case .hasContentTransition: + transition.result = .bool(true) + } + } + } +} + +@_spi(Private) +@available(*, unavailable) +extension SymbolEffect.HiddenTransition: Sendable {} + +// MARK: - ContentTransition + symbolReplace + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension ContentTransition { + public static func symbolReplace(_ config: SymbolEffect.ReplaceConfiguration = .default) -> ContentTransition { + ContentTransition(storage: .symbolReplace(config.base)) + } +} + +// MARK: - SymbolEffect + ProtobufMessage + +@_spi(Private) +extension SymbolEffect: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + try base.encode(to: &encoder) + } + + package init(from decoder: inout ProtobufDecoder) throws { + self.init(base: try _SymbolEffect(from: &decoder)) + } +} + +@_spi(Private) +extension SymbolEffect { + @available(OpenSwiftUI_v6_0, *) + public var isIndefinite: Bool { + base.isIndefinite + } +} diff --git a/Sources/OpenSwiftUI/View/Image/SymbolEffect.swift b/Sources/OpenSwiftUI/View/Image/SymbolEffect.swift deleted file mode 100644 index 4ad8839f2..000000000 --- a/Sources/OpenSwiftUI/View/Image/SymbolEffect.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// SymbolEffect.swift -// OpenSwiftUI -// -// Audited for 6.5.4 -// Status: Empty -// ID: C350E1C6FA5CB264BFEA1DF73B9E8FA7 (SwiftUI) - -public import OpenSwiftUICore - -// MARK: - SymbolEffect [TODO] - -@_spi(Private) -@available(OpenSwiftUI_v5_0, *) -public struct SymbolEffect: Equatable { - // TODO: Store the underlying _SymbolEffect and expose the remaining - // pulse, breathe, bounce, wiggle, rotate, variable color, scale, and - // replace configurations. - - public struct HiddenConfiguration: Equatable { - public static var `default`: SymbolEffect.HiddenConfiguration { - .init() - } - } - - public static func hidden( - _ isHidden: Bool = true, - _ config: SymbolEffect.HiddenConfiguration = .default - ) -> SymbolEffect { - .init() - } -} - -@_spi(Private) -@available(*, unavailable) -extension SymbolEffect.HiddenConfiguration: Sendable {} - -// MARK: - View + symbolEffect - -@_spi(Private) -@available(OpenSwiftUI_v5_0, *) -extension View { - @_alwaysEmitIntoClient - nonisolated public func symbolEffect(_ effect: SymbolEffect?) -> some View { - modifier(_SymbolEffectModifier(effect: effect)) - } -} - -@_spi(Private) -@available(OpenSwiftUI_v6_0, *) -extension View { - @_alwaysEmitIntoClient - nonisolated public func symbolEffects(_ effects: [SymbolEffect]) -> some View { - modifier(_SymbolEffectsModifier(effects: effects)) - } -} - -@available(OpenSwiftUI_v5_0, *) -extension View { - @_alwaysEmitIntoClient - nonisolated public func symbolEffectsRemoved(_ isEnabled: Bool = true) -> some View { - modifier(_SymbolEffectsRemovedModifier(isEnabled: isEnabled)) - } -} - -// MARK: - _SymbolEffectModifier [TODO] - -@_spi(Private) -@available(OpenSwiftUI_v5_0, *) -@frozen -public struct _SymbolEffectModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { - @usableFromInline - package var effect: SymbolEffect? - - @usableFromInline - package init(effect: SymbolEffect?) { - self.effect = effect - } - - public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { - // TODO: Append the effect to the environment symbol effects. - } -} - -@_spi(Private) -@available(*, unavailable) -extension _SymbolEffectModifier: Sendable {} - -// MARK: - _SymbolEffectsModifier [TODO] - -@_spi(Private) -@available(OpenSwiftUI_v6_0, *) -@frozen -public struct _SymbolEffectsModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { - @usableFromInline - var effects: [SymbolEffect] - - @_alwaysEmitIntoClient - init(effects: [SymbolEffect]) { - self.effects = effects - } - - public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { - // TODO: Append the effects to the environment symbol effects. - } -} - -@_spi(Private) -@available(*, unavailable) -extension _SymbolEffectsModifier: Sendable {} - -// MARK: - _SymbolEffectsRemovedModifier [TODO] - -@available(OpenSwiftUI_v5_0, *) -@frozen -public struct _SymbolEffectsRemovedModifier: ViewModifier, PrimitiveViewModifier, _GraphInputsModifier { - @usableFromInline - var isEnabled: Bool - - @_alwaysEmitIntoClient - init(isEnabled: Bool) { - self.isEnabled = isEnabled - } - - public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { - // TODO: Clear the environment symbol effects when enabled. - } -} diff --git a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift index abf4754f3..f2c2987a0 100644 --- a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift +++ b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift @@ -590,6 +590,7 @@ package enum ORBTransitionEffectType: UInt32, Equatable { case matchMove = 5 case translationScale = 15 case relativeBlur = 16 + case symbolAppearDisappear = 17 } // MARK: - DisablesContentTransitionsKey diff --git a/Sources/OpenSwiftUICore/View/Image/Image.swift b/Sources/OpenSwiftUICore/View/Image/Image.swift index c82877e39..a3a2f1eb0 100644 --- a/Sources/OpenSwiftUICore/View/Image/Image.swift +++ b/Sources/OpenSwiftUICore/View/Image/Image.swift @@ -556,44 +556,6 @@ final package class ImageProviderBox: AnyImageProviderBox, @unchecked Send } } -// FIXME - -extension ORBSymbolAnimator { - // TODO: 2975F89CBD28662DFA5DA6D958CBE343 - @discardableResult - fileprivate func addAnimation( - _ animation: ORBSymbolAnimation, - options: [ORBSymbolAnimation.OptionKey: Any], - animationListener: AnimationListener?, - logicalListener: AnimationListener? - ) -> UInt32 { - _openSwiftUIUnimplementedFailure() - } -} - -struct ORBSymbolAnimation: RawRepresentable { - let rawValue: Int - - /// Variable value change - static let variableValue = ORBSymbolAnimation(rawValue: 0x0) - /// Symbol replace animation - static let replace = ORBSymbolAnimation(rawValue: 0x6) - /// Interpolate animation - static let interpolate = ORBSymbolAnimation(rawValue: 0x7) - - struct OptionKey: RawRepresentable, Hashable { - let rawValue: String - } -} - -extension ORBSymbolAnimation.OptionKey { - static let timing = ORBSymbolAnimation.OptionKey(rawValue: "ORBSymbolAnimationTiming") - static let interpolateOptions = ORBSymbolAnimation.OptionKey(rawValue: "ORBSymbolAnimationInterpolateOptions") - static let replaceOptions = ORBSymbolAnimation.OptionKey(rawValue: "ORBSymbolAnimationReplaceOptions") - static let byLayer = ORBSymbolAnimation.OptionKey(rawValue: "ORBSymbolAnimationByLayer") - static let speed = ORBSymbolAnimation.OptionKey(rawValue: "ORBSymbolAnimationSpeed") -} - extension Animation { fileprivate var rbAnimation: ORBAnimation { let animation = ORBAnimation() @@ -605,9 +567,9 @@ extension Animation { extension _SymbolEffect.ReplaceConfiguration { fileprivate var animationOptions: [ORBSymbolAnimation.OptionKey: Any] { [ - .replaceOptions: self, -// .byLayer: byLayer, -// .speed: speed, + .replaceOptions: flags.rawValue, + .byLayer: layered, + .speed: speed, ] } } From faf1905c35c18b9e93edc1d251eaca416b001d6a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 24 Oct 2026 16:55:57 +0800 Subject: [PATCH 5/5] build(deps): update symbol animation dependencies --- Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6f5b1b73b..78b23b74d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "ebf635d498be7e26ddb308ea5d17ca28a6754dd6" + "revision" : "d46803e009851e31ff8190fcf2cfc9dee3ed1e0e" } }, { @@ -43,7 +43,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenRenderBox", "state" : { "branch" : "main", - "revision" : "f94140bc0794a073e3352618c421a14b1cda15d1" + "revision" : "e5ad62adf14b6fd3546a4a6f5c87f4ece1b893c0" } }, {