From 187ef4139020d1b1cf2f2aac064e3f6d58cd3923 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Jan 2026 00:32:21 +0800 Subject: [PATCH 1/7] Add InterpolatableContent --- .../Transition/ContentTransition.swift | 142 +++++++++++++++++- .../Render/InterpolatableContent.swift | 67 +++++++++ .../OpenSwiftUICore/Render/SymbolEffect.swift | 5 + .../View/Image/ResolvedImage.swift | 40 ++--- 4 files changed, 227 insertions(+), 27 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Render/InterpolatableContent.swift diff --git a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift index e9bb03d5c..b7933e62c 100644 --- a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift +++ b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift @@ -1,27 +1,46 @@ // // ContentTransition.swift // OpenSwiftUICore - -// TODO +// +// Audited for 6.5.4 +// Status: WIP public import OpenCoreGraphicsShims package import OpenRenderBoxShims +// MARK: - ContentTransition [WIP] + +/// A kind of transition that applies to the content within a single view, +/// rather than to the insertion or removal of a view. +/// +/// Set the behavior of content transitions within a view with the +/// ``View/contentTransition(_:)`` modifier, passing in one of the defined +/// transitions, such as ``opacity`` or ``interpolate`` as the parameter. +/// +/// > Tip: Content transitions only take effect within transactions that apply +/// an ``Animation`` to the views inside the ``View/contentTransition(_:)`` +/// modifier. +/// +/// Content transitions only take effect within the context of an +/// ``Animation`` block. @available(OpenSwiftUI_v4_0, *) public struct ContentTransition: Equatable, Sendable { + + // MARK: - ContentTransition.Storage + package enum Storage: Equatable, @unchecked Sendable { case named(ContentTransition.NamedTransition) // case custom(ContentTransition.CustomTransition) -// case symbolReplace(_SymbolEffect.ReplaceConfiguration) + case symbolReplace(_SymbolEffect.ReplaceConfiguration) } + // MARK: - ContentTransition.Style + @_spi(Private) public struct Style: Hashable, Sendable/*, Codable*/ { package enum Storage: Hashable, Sendable { case `default` - case sessionWidget - case animatedWidget } @@ -44,9 +63,12 @@ public struct ContentTransition: Equatable, Sendable { package var isReplaceable: Bool package init(storage: ContentTransition.Storage) { - _openSwiftUIUnimplementedFailure() + self.storage = storage + self.isReplaceable = false } + // MARK: - ContentTransition.NamedTransition + package struct NamedTransition: Hashable, Sendable { package enum Name: Hashable { case `default` @@ -55,7 +77,7 @@ public struct ContentTransition: Equatable, Sendable { case diff case fadeIfDifferent case text(different: Bool) - // case numericText(ContentTransition.NumericTextConfiguration) + case numericText(ContentTransition.NumericTextConfiguration) } package var name: ContentTransition.NamedTransition.Name @@ -73,7 +95,78 @@ public struct ContentTransition: Equatable, Sendable { } } - // TODO: NumericTextConfiguration + // MARK: - ContentTransition.NumericTextConfiguration [WIP] + + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public struct NumericTextConfiguration: Hashable, Sendable { + package enum Direction: Hashable { + case fixed(downwards: Bool) + case automatic(value: Float) + } + + package struct Options: OptionSet, Hashable { + package let rawValue: UInt8 + + package init(rawValue: UInt8) { + self.rawValue = rawValue + } + + package static let reversed: ContentTransition.NumericTextConfiguration.Options = .init(rawValue: 1 << 0) + + package static let relativeBlur: ContentTransition.NumericTextConfiguration.Options = .init(rawValue: 1 << 1) + } + + package var direction: ContentTransition.NumericTextConfiguration.Direction + package var axis: Axis? + package var options: ContentTransition.NumericTextConfiguration.Options + private var _delay: UInt8 = 18 + private var _scale: UInt8 = 51 + private var _blur: UInt8 = 32 + private var _offset: UInt8 = 19 + + @_spi(Private) + package init( + direction: ContentTransition.NumericTextConfiguration.Direction = .fixed(downwards: false), + axis: Axis? = nil, + options: ContentTransition.NumericTextConfiguration.Options = .relativeBlur + ) { + self.direction = direction + self.axis = axis + self.options = options + } + + @_spi(Private) + package var delay: Float { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package var maxDurationMultiple: Float { + _openSwiftUIUnimplementedFailure() + } + + package var scale: Float { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package var blur: Float { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package var relativeBlur: Float { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package var offset: Float { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + } + @_spi(Private) public struct EffectType: Equatable, Sendable { @@ -225,6 +318,39 @@ public struct ContentTransition: Equatable, Sendable { // TODO package struct State {} + + /// The identity content transition, which indicates that content changes + /// shouldn't animate. + /// + /// You can pass this value to a ``View/contentTransition(_:)`` + /// modifier to selectively disable animations that would otherwise + /// be applied by a ``withAnimation(_:_:)`` block. + public static let identity: ContentTransition = .init(storage: .named(.init())) // FIXME + + /// A content transition that indicates content fades from transparent + /// to opaque on insertion, and from opaque to transparent on removal. + public static let opacity: ContentTransition = .init(storage: .named(.init())) // FIXME + + /// A content transition that indicates the views attempt to interpolate + /// their contents during transitions, where appropriate. + /// + /// Text views can interpolate transitions when the text views have + /// identical strings. Matching glyph pairs can animate changes to their + /// color, position, size, and any variable properties. Interpolation can + /// apply within a ``Font/Design`` case, but not between cases, or between + /// entirely different fonts. For example, you can interpolate a change + /// between ``Font/Weight/thin`` and ``Font/Weight/black`` variations of a + /// font, since these are both cases of ``Font/Weight``. However, you can't + /// interpolate between the default design of a font and its Italic version, + /// because these are different fonts. Any changes that can't show an + /// interpolated animation use an opacity animation instead. + /// + /// Symbol images created with the ``Image/init(systemName:)`` initializer + /// work the same way as text: changes within the same symbol attempt to + /// interpolate the symbol's paths. When interpolation is unavailable, the + /// system uses an opacity transition instead. + public static let interpolate: ContentTransition = .init(storage: .named(.init())) // FIXME + } // FIXME: ORB diff --git a/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift new file mode 100644 index 000000000..7fea35e42 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift @@ -0,0 +1,67 @@ +// +// InterpolatableContent.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 7377A3587909D054D379011E12826F37 (SwiftUICore) + +package import OpenAttributeGraphShims + +package protocol InterpolatableContent { + static var defaultTransition: ContentTransition { get } + + func requiresTransition(to other: Self) -> Bool + + var appliesTransitionsForSizeChanges: Bool { get } + + var addsDrawingGroup: Bool { get } + + func modifyTransition(state: inout ContentTransition.State, to other: Self) + + func defaultAnimation(to other: Self) -> Animation? +} + +extension InterpolatableContent where Self: Equatable { + package func requiresTransition(to other: Self) -> Bool { + self != other + } + + package var appliesTransitionsForSizeChanges: Bool { + false + } + + package var addsDrawingGroup: Bool { + false + } +} + +extension InterpolatableContent { + package static var defaultTransition: ContentTransition { + .identity + } + + package func modifyTransition(state: inout ContentTransition.State, to other: Self) { + _openSwiftUIEmptyStub() + } + + package func defaultAnimation(to other: Self) -> Animation? { + nil + } +} + +extension _ViewOutputs { + package mutating func applyInterpolatorGroup( + _ group: DisplayList.InterpolatorGroup, + content: Attribute, + inputs: _ViewInputs, + animatesSize: Bool, + defersRender: Bool + ) where T: InterpolatableContent { + _openSwiftUIUnimplementedFailure() + } +} + +private struct InterpolatedDisplayList where Content: InterpolatableContent { + +} diff --git a/Sources/OpenSwiftUICore/Render/SymbolEffect.swift b/Sources/OpenSwiftUICore/Render/SymbolEffect.swift index 2bdab2b50..5518017af 100644 --- a/Sources/OpenSwiftUICore/Render/SymbolEffect.swift +++ b/Sources/OpenSwiftUICore/Render/SymbolEffect.swift @@ -8,6 +8,11 @@ import OpenAttributeGraphShims package struct _SymbolEffect: Equatable { + package struct ReplaceConfiguration: Equatable { + package init() { + // TODO + } + } } extension _SymbolEffect { diff --git a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift index 156e922da..7a082afb7 100644 --- a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift @@ -141,7 +141,7 @@ extension Image { } } -// MARK: - Image.Resolved + View [_makeView WIP] +// MARK: - Image.Resolved + View [mustUpdate & _makeView WIP] extension Image.Resolved: UnaryView, PrimitiveView, ShapeStyledLeafView, LeafViewLayout { package struct UpdateData { @@ -253,14 +253,13 @@ extension Image.Resolved: UnaryView, PrimitiveView, ShapeStyledLeafView, LeafVie interpolatorGroup: group, data: data ) - // TODO: InterpolatableContent for Image.Resolved -// outputs.applyInterpolatorGroup( -// group, -// content:view.value, -// inputs: inputs, -// animatesSize: true, -// defersRender: false -// ) + outputs.applyInterpolatorGroup( + group, + content:view.value, + inputs: inputs, + animatesSize: true, + defersRender: false + ) } } else { outputs = .init() @@ -373,17 +372,20 @@ private struct ResolvedImageLayoutEngine: LayoutEngine { } } -// MARK: - Image.Resolved + InterpolatableContent [TODO] +// MARK: - Image.Resolved + InterpolatableContent [WIP] -//extension Image.Resolved: InterpolatableContent { -// package static var defaultTransition: ContentTransition { -// _openSwiftUIUnimplementedFailure() -// } -// -// package func modifyTransition(state: inout ContentTransition.State, to other: Image.Resolved) { -// _openSwiftUIUnimplementedFailure() -// } -//} +extension Image.Resolved: InterpolatableContent { + package static var defaultTransition: ContentTransition { + isLinkedOnOrAfter(.v4) ? .interpolate : .identity + } + + package func modifyTransition( + state: inout ContentTransition.State, + to other: Image.Resolved + ) { + _openSwiftUIUnimplementedWarning() + } +} extension EnvironmentValues { package func imageIsTemplate(renderingMode: Image.TemplateRenderingMode? = nil) -> Bool { From 226a47a516256cfeb0ebc439844e35ab9237ce0a Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Jan 2026 01:01:49 +0800 Subject: [PATCH 2/7] Add InterpolatedDisplayList --- .../Render/InterpolatableContent.swift | 105 +++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift index 7fea35e42..e78cde052 100644 --- a/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift +++ b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift @@ -3,11 +3,13 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 7377A3587909D054D379011E12826F37 (SwiftUICore) package import OpenAttributeGraphShims +// MARK: - InterpolatableContent + package protocol InterpolatableContent { static var defaultTransition: ContentTransition { get } @@ -50,6 +52,8 @@ extension InterpolatableContent { } } +// MARK: - _ViewOutputs + applyInterpolatorGroup + extension _ViewOutputs { package mutating func applyInterpolatorGroup( _ group: DisplayList.InterpolatorGroup, @@ -58,10 +62,105 @@ extension _ViewOutputs { animatesSize: Bool, defersRender: Bool ) where T: InterpolatableContent { - _openSwiftUIUnimplementedFailure() + guard let list = preferences.displayList else { + return + } + let interpolatedDisplayList = Attribute( + InterpolatedDisplayList( + group: group, + content: content, + position: inputs.position, + animatedPosition: inputs.animatedPosition(), + containerPosition: inputs.containerPosition, + size: inputs.size.cgSize, + phase: inputs.viewPhase, + time: inputs.time, + transaction: inputs.transaction, + environment: inputs.environment, + pixelLength: inputs.pixelLength, + list: .init(list), + animatesSize: animatesSize, + defersRender: defersRender, + supportsVFD: inputs.supportsVFD, + lastContent: nil, + lastSize: .zero, + resetSeed: .zero, + contentVersion: .init() + ) + ) + interpolatedDisplayList.flags = .transactional + displayList = interpolatedDisplayList } } -private struct InterpolatedDisplayList where Content: InterpolatableContent { +// MARK: - InterpolatedDisplayList + +private struct InterpolatedDisplayList: StatefulRule, AsyncAttribute where Content: InterpolatableContent { + let group: DisplayList.InterpolatorGroup + @Attribute var content: Content + @Attribute var position: CGPoint + @Attribute var animatedPosition: CGPoint + @Attribute var containerPosition: CGPoint + @Attribute var size: CGSize + @Attribute var phase: _GraphInputs.Phase + @Attribute var time: Time + @Attribute var transaction: Transaction + @Attribute var environment: EnvironmentValues + @Attribute var pixelLength: CGFloat + @OptionalAttribute var list: DisplayList? + let animatesSize: Bool + let defersRender: Bool + let supportsVFD: Bool + var lastContent: Content? + var lastSize: CGSize + var resetSeed: UInt32 + var contentVersion: DisplayList.Version + + init( + group: DisplayList.InterpolatorGroup, + content: Attribute, + position: Attribute, + animatedPosition: Attribute, + containerPosition: Attribute, + size: Attribute, + phase: Attribute<_GraphInputs.Phase>, + time: Attribute