From 15f85b5b629de17ec8e03b5b504f93b3c5b775d6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 30 Jun 2025 00:15:40 +0800 Subject: [PATCH 1/8] Update DisplayList.Key usage --- .../Render/DisplayList/DisplayList.swift | 24 +++++++++++++++++++ .../Render/RendererLeafView.swift | 2 +- .../View/Graph/ViewGraph.swift | 4 ++-- .../View/Input/ViewInputs.swift | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index bc2b63271..f4e84c052 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -7,6 +7,7 @@ // ID: F37E3733E490AA5E3BDC045E3D34D9F8 (SwiftUICore) package import Foundation +import OpenGraphShims // MARK: - _DisplayList_Identity @@ -428,6 +429,29 @@ extension DisplayList { } } +extension PreferencesInputs { + @inline(__always) + var requiresDisplayList: Bool { + get { + contains(DisplayList.Key.self) + } + set { + if newValue { + add(DisplayList.Key.self) + } else { + remove(DisplayList.Key.self) + } + } + } +} + +extension PreferencesOutputs { + var displayList: Attribute? { + get { self[DisplayList.Key.self] } + set { self[DisplayList.Key.self] = newValue } + } +} + // MARK: - DisplayList.Item + Extension extension DisplayList.Item { diff --git a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift index f6e408fe9..bcb2fe52d 100644 --- a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift +++ b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift @@ -29,7 +29,7 @@ extension RendererLeafView { // TODO var outputs = _ViewOutputs() // FIXME - outputs.preferences[DisplayList.Key.self] = Attribute( + outputs.preferences.displayList = Attribute( LeafDisplayList( identity: .init(), view: view.value, diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift index b8b5b13fd..cf420fbfb 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift @@ -35,7 +35,7 @@ package final class ViewGraph: GraphHost { fileprivate func addRequestedPreferences(to inputs: inout _ViewInputs) { inputs.preferences.add(HostPreferencesKey.self) if contains(.displayList) { - inputs.preferences.add(DisplayList.Key.self) + inputs.preferences.requiresDisplayList = true } if contains(.viewResponders) { inputs.preferences.requiresViewResponders = true @@ -257,7 +257,7 @@ package final class ViewGraph: GraphHost { rootGeometry.$childLayoutComputer = outputs.layoutComputer } if requestedOutputs.contains(.displayList) { - if let displayList = outputs.preferences[DisplayList.Key.self] { + if let displayList = outputs.preferences.displayList { _rootDisplayList = WeakAttribute(rootSubgraph.apply { Attribute(RootDisplayList(content: displayList, time: data.$time)) }) diff --git a/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift b/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift index 801b07c71..5b78ae28a 100644 --- a/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift +++ b/Sources/OpenSwiftUICore/View/Input/ViewInputs.swift @@ -237,7 +237,7 @@ extension _ViewInputs { inputs.size = viewGraph.intern(ViewSize.zero, id: .defaultValue) inputs.requestsLayoutComputer = false inputs.needsGeometry = false - inputs.preferences.remove(DisplayList.Key.self) + inputs.preferences.requiresDisplayList = false inputs.preferences.requiresViewResponders = false return inputs } From 09d1cdbf50e563e7c73c40c6793bc7b7084ab47f Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 30 Jun 2025 00:15:52 +0800 Subject: [PATCH 2/8] Add RendererEffect --- .../RendererEffect/RendererEffect.swift | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Render/RendererEffect/RendererEffect.swift diff --git a/Sources/OpenSwiftUICore/Render/RendererEffect/RendererEffect.swift b/Sources/OpenSwiftUICore/Render/RendererEffect/RendererEffect.swift new file mode 100644 index 000000000..3ff606281 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/RendererEffect/RendererEffect.swift @@ -0,0 +1,93 @@ +// +// RendererEffect.swift +// OpenSwiftUICore +// +// Status: WIP +// ID: 49800242E3DD04CB91F7CE115272DDC3 (SwiftUICore) + +package import Foundation + +// MARK: - _RendererEffect [6.5.4] [WIP] + +package protocol _RendererEffect: MultiViewModifier, PrimitiveViewModifier { + func effectValue(size: CGSize) -> DisplayList.Effect + + static var isolatesChildPosition: Bool { get } + + static var disabledForFlattenedContent: Bool { get } + + static var preservesEmptyContent: Bool { get } + + static var isScrapeable: Bool { get } + + // var scrapeableContent: ScrapeableContent.Content? { get } +} + +extension _RendererEffect { + package static var isolatesChildPosition: Bool { + false + } + + package static var disabledForFlattenedContent: Bool { + false + } + + package static var preservesEmptyContent: Bool { + false + } + + package static var isScrapeable: Bool { + false + } + +// package var scrapeableContent: ScrapeableContent.Content? { +// nil +// } + + package static func _makeRendererEffect( + effect: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + if isScrapeable { + // TOOD: Handle scrapeable content + } + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - RendererEffect [6.5.4] + +package protocol RendererEffect: Animatable, _RendererEffect {} + +@available(OpenSwiftUI_v1_0, *) +extension RendererEffect { + package static func makeRendererEffect( + effect: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + guard inputs.needsGeometry || inputs.preferences.requiresDisplayList else { + return body(_Graph(), inputs) + } + var effect = effect + _makeAnimatable(value: &effect, inputs: inputs.base) + return _makeRendererEffect(effect: effect, inputs: inputs, body: body) + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + makeRendererEffect(effect: modifier, inputs: inputs, body: body) + } + + @available(OpenSwiftUI_v2_0, *) + nonisolated public static func _viewListCount( + inputs: _ViewListCountInputs, + body: (_ViewListCountInputs) -> Int? + ) -> Int? { + body(inputs) + } +} From fc3a82f7410777c53a33c7ab075eebefe7710f42 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 30 Jun 2025 02:11:43 +0800 Subject: [PATCH 3/8] Add GeometryEffect implementation --- .../Render/DisplayList/DisplayList.swift | 9 +- .../GeometryEffect/GeometryEffect.swift | 219 ++++++++++++++++++ 2 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index f4e84c052..798e35f3f 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -6,6 +6,7 @@ // Status: WIP // ID: F37E3733E490AA5E3BDC045E3D34D9F8 (SwiftUICore) +package import CoreGraphicsShims package import Foundation import OpenGraphShims @@ -99,7 +100,11 @@ package struct DisplayList: Equatable { // TODO items.append(contentsOf: other.items) // _openSwiftUIUnimplementedFailure() - } + } + + package var isEmpty: Bool { + items.isEmpty + } } @available(*, unavailable) @@ -205,9 +210,7 @@ extension DisplayList { } package enum Transform { - #if canImport(Darwin) case affine(CGAffineTransform) - #endif case projection(ProjectionTransform) // case rotation(_RotationEffect.Data) // case rotation3D(_Rotation3DEffect.Data) diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift new file mode 100644 index 000000000..6160ff517 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift @@ -0,0 +1,219 @@ +// +// GeometryEffect.swift +// OpenSwiftUICore +// +// Status: WIP +// ID: 9ED0B9F1F6CE74691B78276C750FEDD3 (SwiftUICore) + +public import Foundation +package import OpenGraphShims + +// MARK: - GeometryEffect [6.5.4] [WIP] + +/// An effect that changes the visual appearance of a view, largely without +/// changing its ancestors or descendants. +/// +/// The only change the effect makes to the view's ancestors and descendants is +/// to change the coordinate transform to and from them. +@available(OpenSwiftUI_v1_0, *) +public protocol GeometryEffect: Animatable, ViewModifier where Body == Never { + /// Returns the current value of the effect. + func effectValue(size: CGSize) -> ProjectionTransform + + /// If false the effect's transform is not applied to coordinate + /// space conversions crossing the view, only to the renderered + /// representation of the child view. + static var _affectsLayout: Bool { get } +} + +@available(OpenSwiftUI_v1_0, *) +extension GeometryEffect { + public static var _affectsLayout: Bool { + true + } +} + +@available(OpenSwiftUI_v1_0, *) +extension GeometryEffect { + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + makeGeometryEffect(modifier: modifier, inputs: inputs, body: body) + } + + nonisolated package static func makeGeometryEffect( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + _openSwiftUIUnimplementedFailure() + } + + nonisolated public static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + makeMultiViewList(modifier: modifier, inputs: inputs, body: body) + } + + @available(OpenSwiftUI_v2_0, *) + nonisolated public static func _viewListCount( + inputs: _ViewListCountInputs, + body: (_ViewListCountInputs) -> Int? + ) -> Int? { + body(inputs) + } +} + +// MARK: - GeometryEffectProvider [6.5.4] + +protocol GeometryEffectProvider { + associatedtype Effect: GeometryEffect + + static func resolve( + effect: Effect, + origin: inout CGPoint, + size: CGSize, + layoutDirection: LayoutDirection + ) -> DisplayList.Effect +} + +// MARK: - RoundedSize [6.5.4] + +package struct RoundedSize: Rule, AsyncAttribute { + @Attribute var position: CGPoint + @Attribute var size: ViewSize + @Attribute var pixelLength: CGFloat + + package init( + position: Attribute, + size: Attribute, + pixelLength: Attribute + ) { + _position = position + _size = size + _pixelLength = pixelLength + } + + package var value: ViewSize { + var size = size + var rect = CGRect(origin: position, size: size.value) + rect.roundCoordinatesToNearestOrUp(toMultipleOf: pixelLength) + size.value = rect.size + return size + } +} + +// MARK: - DefaultGeometryEffectProvider [6.5.4] + +struct DefaultGeometryEffectProvider: GeometryEffectProvider where Effect: GeometryEffect { + static func resolve( + effect: Effect, + origin: inout CGPoint, + size: CGSize, + layoutDirection: LayoutDirection + ) -> DisplayList.Effect { + var effectValue = effect.effectValue(size: size) + if layoutDirection == .rightToLeft { + let t = ProjectionTransform( + m11: -1, m12: 0, m13: 0, + m21: 0, m22: 1, m23: 0, + m31: size.width, m32: 0, m33: 1 + ) + effectValue = t + .concatenating(effectValue) + .concatenating(t) + } + guard effectValue.isInvertible else { + Log.externalWarning("ignoring singular matrix: \(effectValue)") + return .identity + } + if effectValue.isAffine { + return .transform(.affine(.init(effectValue))) + } else { + return .transform(.projection(effectValue)) + } + } +} + +// MARK: - GeometryEffectDisplayList [6.5.4] + +private struct GeometryEffectDisplayList: Rule, AsyncAttribute, CustomStringConvertible + where Provider: GeometryEffectProvider { + let identity: DisplayList.Identity + @Attribute var effect: Provider.Effect + @Attribute var position: CGPoint + @Attribute var size: CGSize + @Attribute var layoutDirection: LayoutDirection + @Attribute var containerPosition: CGPoint + @OptionalAttribute var content: DisplayList? + let options: DisplayList.Options + + var value: DisplayList { + let content = content ?? DisplayList() + guard !content.isEmpty else { + return content + } + var origin = CGPoint(position - containerPosition) + let displayListEffect = Provider.resolve( + effect: effect, + origin: &origin, + size: size, + layoutDirection: layoutDirection + ) + var item = DisplayList.Item( + .effect(displayListEffect, content), + frame: CGRect(origin: origin, size: size), + identity: identity, + version: .init(forUpdate: ()) + ) + item.canonicalize(options: options) + return DisplayList(item) + } + + var description: String { + "GeometryEffectDisplayList" + } +} + +// MARK: - GeometryEffectTransform [6.5.4] + +private struct GeometryEffectTransform: Rule, AsyncAttribute where Effect: GeometryEffect { + @Attribute var effect: Effect + @Attribute var size: CGSize + @Attribute var position: CGPoint + @Attribute var transform: ViewTransform + @Attribute var layoutDirection: LayoutDirection + + typealias Value = ViewTransform + + var value: Value { + var transform = transform + transform.resetPosition(position) + if Effect._affectsLayout { + var effectValue = effect.effectValue(size: size) + if layoutDirection == .rightToLeft { + let t = ProjectionTransform( + m11: -1, m12: 0, m13: 0, + m21: 0, m22: 1, m23: 0, + m31: size.width, m32: 0, m33: 1 + ) + effectValue = t + .concatenating(effectValue) + .concatenating(t) + } + if effectValue.isInvertible { + transform.appendProjectionTransform( + effectValue, + inverse: true + ) + } else { + Log.externalWarning("ignoring singular matrix: \(effectValue)") + } + } + return transform + } +} From 16b1e0c438bdafc00412394a17fa49a4ff1749c2 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 30 Jun 2025 10:36:36 +0800 Subject: [PATCH 4/8] Add GeometryEffectProvider._makeGeometryEffect --- .../Environment/EnvironmentAdditions.swift | 7 ++ .../Render/DisplayList/DisplayList.swift | 7 +- .../DisplayList_StableIdentity.swift | 36 ++++++---- .../GeometryEffect/GeometryEffect.swift | 69 ++++++++++++++++++- .../GeometryEffect/Rotation3DEffect.swift | 7 ++ .../GeometryEffect/RotationEffect.swift | 7 ++ 6 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Render/GeometryEffect/Rotation3DEffect.swift create mode 100644 Sources/OpenSwiftUICore/Render/GeometryEffect/RotationEffect.swift diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentAdditions.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentAdditions.swift index cf7059db8..383d07eb4 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentAdditions.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentAdditions.swift @@ -7,6 +7,7 @@ // ID: 1B17C64D9E901A0054B49B69A4A2439D (SwiftUICore) public import Foundation +package import OpenGraphShims // MARK: - EnvironmentValues + Display [6.4.41] @@ -43,6 +44,12 @@ extension CachedEnvironment.ID { package static let pixelLength: CachedEnvironment.ID = .init() } +extension _ViewInputs { + package var pixelLength: Attribute { + mapEnvironment(id: .pixelLength) { $0.pixelLength } + } +} + private struct DisplayGamutKey: EnvironmentKey { static var defaultValue: DisplayGamut { .sRGB } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index 798e35f3f..68589eb66 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -8,7 +8,7 @@ package import CoreGraphicsShims package import Foundation -import OpenGraphShims +package import OpenGraphShims // MARK: - _DisplayList_Identity @@ -434,7 +434,7 @@ extension DisplayList { extension PreferencesInputs { @inline(__always) - var requiresDisplayList: Bool { + package var requiresDisplayList: Bool { get { contains(DisplayList.Key.self) } @@ -449,7 +449,8 @@ extension PreferencesInputs { } extension PreferencesOutputs { - var displayList: Attribute? { + @inline(__always) + package var displayList: Attribute? { get { self[DisplayList.Key.self] } set { self[DisplayList.Key.self] = newValue } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList_StableIdentity.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList_StableIdentity.swift index d5ff001f5..b0918b7dd 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList_StableIdentity.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList_StableIdentity.swift @@ -95,12 +95,24 @@ extension _DisplayList_StableIdentityMap: ProtobufMessage { } } -// TODO: Blocked by _ViewInputs -//extension _ViewInputs { -// package mutating func configureStableIDs(root: _DisplayList_StableIdentityRoot) { -// package func pushIdentity(_ identity: _DisplayList_Identity) -// package func makeStableIdentity() -> _DisplayList_StableIdentity -//} +extension _ViewInputs { + package mutating func configureStableIDs(root: _DisplayList_StableIdentityRoot) { + _openSwiftUIUnimplementedFailure() + } + + package func pushIdentity(_ identity: _DisplayList_Identity) { + + guard base.needsStableDisplayListIDs else { + return + } + self[_DisplayList_StableIdentityScope.self].attribute!.value.pushIdentity(identity) + } + + package func makeStableIdentity() -> _DisplayList_StableIdentity { + // Log.internalError("expected stable IDs to be supported") + _openSwiftUIUnimplementedFailure() + } +} extension _GraphInputs { private func pushScope(id: ID) where ID: StronglyHashable { @@ -110,7 +122,7 @@ extension _GraphInputs { } package mutating func pushStableID(_ id: ID) where ID: Hashable { - guard options.contains(.needsStableDisplayListIDs) else { + guard needsStableDisplayListIDs else { return } if let stronglyHashable = id as? StronglyHashable { @@ -121,23 +133,21 @@ extension _GraphInputs { } package mutating func pushStableIndex(_ index: Int) { - guard options.contains(.needsStableDisplayListIDs) else { + guard needsStableDisplayListIDs else { return } pushScope(id: index) } package mutating func pushStableType(_ type: any Any.Type) { - #if OPENSWIFTUI_SUPPORT_2024_API - guard options.contains(.needsStableDisplayListIDs) else { + guard needsStableDisplayListIDs else { return } pushScope(id: makeStableTypeData(type)) - #endif } package var stableIDScope: WeakAttribute? { - guard !options.contains(.needsStableDisplayListIDs) else { + guard !needsStableDisplayListIDs else { // Question return nil } let result = self[_DisplayList_StableIdentityScope.self] @@ -145,11 +155,9 @@ extension _GraphInputs { } } -#if OPENSWIFTUI_SUPPORT_2024_API package func makeStableTypeData(_ type: any Any.Type) -> StrongHash { unsafeBitCast(Metadata(type).signature, to: StrongHash.self) } -#endif package func makeStableIDData(from id: ID) -> StrongHash? { guard let encodable = id as? Encodable else { diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift index 6160ff517..908ea34d0 100644 --- a/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift @@ -48,7 +48,18 @@ extension GeometryEffect { inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs ) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() + if modifier is _GraphValue<_RotationEffect> { + _openSwiftUIUnimplementedFailure() + } else if modifier is _GraphValue<_Rotation3DEffect> { + _openSwiftUIUnimplementedFailure() + } else { + DefaultGeometryEffectProvider + ._makeGeometryEffect( + modifier: modifier, + inputs: inputs, + body: body + ) + } } nonisolated public static func _makeViewList( @@ -81,6 +92,62 @@ protocol GeometryEffectProvider { ) -> DisplayList.Effect } +extension GeometryEffectProvider { + static func _makeGeometryEffect( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + guard inputs.needsGeometry else { + return body(_Graph(), inputs) + } + let animatableEffect = Effect.makeAnimatable(value: modifier, inputs: inputs.base) + let transform = Attribute( + GeometryEffectTransform( + effect: animatableEffect, + size: inputs.animatedCGSize(), + position: inputs.animatedPosition(), + transform: inputs.transform, + layoutDirection: inputs.layoutDirection + ) + ) + let size = Attribute( + RoundedSize( + position: inputs.position, + size: inputs.size, + pixelLength: inputs.pixelLength + ) + ) + var newInputs = inputs + let zeroPoint = ViewGraph.current.$zeroPoint + newInputs.transform = transform + newInputs.position = zeroPoint + newInputs.containerPosition = zeroPoint + newInputs.size = size + var outputs = body(_Graph(), newInputs) + guard inputs.preferences.requiresDisplayList else { + return outputs + } + let identity = DisplayList.Identity() + inputs.pushIdentity(identity) + let displayList = Attribute( + GeometryEffectDisplayList( + identity: .init(), + effect: animatableEffect, + position: inputs.animatedPosition(), + size: inputs.animatedCGSize(), // Verify: Still get a new size here + layoutDirection: inputs.layoutDirection, + containerPosition: inputs.containerPosition, + content: .init(outputs.preferences.displayList), + options: .init() + ) + ) + outputs.preferences.displayList = displayList + return outputs + } +} + + // MARK: - RoundedSize [6.5.4] package struct RoundedSize: Rule, AsyncAttribute { diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/Rotation3DEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/Rotation3DEffect.swift new file mode 100644 index 000000000..115c83c27 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/Rotation3DEffect.swift @@ -0,0 +1,7 @@ +import Foundation + +struct _Rotation3DEffect: GeometryEffect { + func effectValue(size: CGSize) -> ProjectionTransform { + .init() + } +} diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/RotationEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/RotationEffect.swift new file mode 100644 index 000000000..43ed2c895 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/RotationEffect.swift @@ -0,0 +1,7 @@ +import Foundation + +struct _RotationEffect: GeometryEffect { + func effectValue(size: CGSize) -> ProjectionTransform { + .init() + } +} From 5c382c0f7ecf4df61ef97d0c7b56d2e3fd4569ce Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 10 Jul 2025 02:07:40 +0800 Subject: [PATCH 5/8] Add IgnoredByLayoutEffect implementation --- .../IgnoredByLayoutEffect.swift | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift diff --git a/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift b/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift new file mode 100644 index 000000000..04601704c --- /dev/null +++ b/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift @@ -0,0 +1,53 @@ +// +// IgnoredByLayoutEffect.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +/// A geometry effect type that prevents another geometry effect +/// affecting coordinate space conversions during layout, i.e. the +/// transform introduced by the other effect is only used when +/// rendering, not when converting locations from one view to another. +/// This is often used to disable layout changes during transitions. +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _IgnoredByLayoutEffect: GeometryEffect where Base: GeometryEffect { + public var base: Base + + public static var _affectsLayout: Bool { false } + + @inlinable + public init(_ base: Base) { + self.base = base + } + + public func effectValue(size: CGSize) -> ProjectionTransform { + base.effectValue(size: size) + } + + public var animatableData: Base.AnimatableData { + get { base.animatableData } + set { base.animatableData = newValue } + } +} + +@available(*, unavailable) +extension _IgnoredByLayoutEffect: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension _IgnoredByLayoutEffect: Equatable where Base: Equatable {} + +@available(OpenSwiftUI_v1_0, *) +extension GeometryEffect { + /// Returns an effect that produces the same geometry transform as this + /// effect, but only applies the transform while rendering its view. + /// + /// Use this method to disable layout changes during transitions. The view + /// ignores the transform returned by this method while the view is + /// performing its layout calculations. + @inlinable + public func ignoredByLayout() -> _IgnoredByLayoutEffect { + return _IgnoredByLayoutEffect(self) + } +} From e732c19c97ff7da7775fd1f0b9b67dccaa4ecb65 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 10 Jul 2025 02:30:57 +0800 Subject: [PATCH 6/8] Add OffsetEffect implementation --- .../Render/GeometryEffect/OffsetEffect.swift | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift new file mode 100644 index 000000000..d495c6b3d --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift @@ -0,0 +1,128 @@ +// +// OffsetEffect.swift +// OpenSwiftUICore +// +// Status: Complete +// ID: 72FB21917F353796516DFC9915156779 (SwiftUICore) + +public import Foundation +import OpenGraphShims + +/// Allows you to redefine origin of the child within its coordinate +/// space +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _OffsetEffect: GeometryEffect, Equatable { + public var offset: CGSize + + @inlinable + nonisolated public init(offset: CGSize) { + self.offset = offset + } + + public func effectValue(size: CGSize) -> ProjectionTransform { + ProjectionTransform( + CGAffineTransform( + translationX: offset.width, + y: offset.height + ) + ) + } + + public var animatableData: CGSize.AnimatableData { + get { offset.animatableData } + set { offset.animatableData = newValue } + } + + nonisolated public static func _makeView( + modifier: _GraphValue<_OffsetEffect>, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var inputs = inputs + inputs.position = Attribute( + OffsetPosition( + effect: modifier.value, + position: inputs.position, + layoutDirection: inputs.layoutDirection + ) + ) + return body(_Graph(), inputs) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Offset this view by the horizontal and vertical amount specified in the + /// offset parameter. + /// + /// Use `offset(_:)` to shift the displayed contents by the amount + /// specified in the `offset` parameter. + /// + /// The original dimensions of the view aren't changed by offsetting the + /// contents; in the example below the gray border drawn by this view + /// surrounds the original position of the text: + /// + /// Text("Offset by passing CGSize()") + /// .border(Color.green) + /// .offset(CGSize(width: 20, height: 25)) + /// .border(Color.gray) + /// + /// ![A screenshot showing a view that offset from its original position a + /// CGPoint to specify the x and y offset.](OpenSwiftUI-View-offset.png) + /// + /// - Parameter offset: The distance to offset this view. + /// + /// - Returns: A view that offsets this view by `offset`. + @inlinable + nonisolated public func offset(_ offset: CGSize) -> some View { + modifier(_OffsetEffect(offset: offset)) + } + + /// Offset this view by the specified horizontal and vertical distances. + /// + /// Use `offset(x:y:)` to shift the displayed contents by the amount + /// specified in the `x` and `y` parameters. + /// + /// The original dimensions of the view aren't changed by offsetting the + /// contents; in the example below the gray border drawn by this view + /// surrounds the original position of the text: + /// + /// Text("Offset by passing horizontal & vertical distance") + /// .border(Color.green) + /// .offset(x: 20, y: 50) + /// .border(Color.gray) + /// + /// ![A screenshot showing a view that offset from its original position + /// using and x and y offset.](openswiftui-offset-xy.png) + /// + /// - Parameters: + /// - x: The horizontal distance to offset this view. + /// - y: The vertical distance to offset this view. + /// + /// - Returns: A view that offsets this view by `x` and `y`. + @inlinable + nonisolated public func offset(x: CGFloat = 0, y: CGFloat = 0) -> some View { + offset(CGSize(width: x, height: y)) + } +} + +private struct OffsetPosition: Rule, AsyncAttribute { + @Attribute var effect: _OffsetEffect + @Attribute var position: CGPoint + @Attribute var layoutDirection: LayoutDirection + + var value: CGPoint { + position.resolved(in: layoutDirection) + effect.offset + } +} + +extension CGPoint { + @inline(__always) + fileprivate func resolved(in layoutDirection: LayoutDirection) -> CGPoint { + switch layoutDirection { + case .leftToRight: CGPoint(x: x, y: y) + case .rightToLeft: CGPoint(x: -x, y: y) + } + } +} From 8f324b88b9c3a3e5ad4c030e6457dac0fa4d7185 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 12 Jul 2025 11:02:19 +0800 Subject: [PATCH 7/8] Add OffsetEffectUITests and IgnoredByLayoutEffectUITests --- .../IgnoredByLayoutEffectUITests.swift | 40 +++++++++++++++++++ .../GeometryEffect/OffsetEffectUITests.swift | 24 +++++++++++ 2 files changed, 64 insertions(+) create mode 100644 Example/OpenSwiftUIUITests/Render/GeometryEffect/IgnoredByLayoutEffectUITests.swift create mode 100644 Example/OpenSwiftUIUITests/Render/GeometryEffect/OffsetEffectUITests.swift diff --git a/Example/OpenSwiftUIUITests/Render/GeometryEffect/IgnoredByLayoutEffectUITests.swift b/Example/OpenSwiftUIUITests/Render/GeometryEffect/IgnoredByLayoutEffectUITests.swift new file mode 100644 index 000000000..0fc9e38d0 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Render/GeometryEffect/IgnoredByLayoutEffectUITests.swift @@ -0,0 +1,40 @@ +// +// IgnoredByLayoutEffectUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct IgnoredByLayoutEffectUITests { + @Test(.disabled("Animation is not implmemented yet")) + func offsetIgnoredByLayout() { + struct ContentView: View { + var body: some View { + WobbleColorView() + } + } + + struct WobbleEffect: GeometryEffect { + var amount: CGFloat = 10 + var shakesPerUnit = 3 + var animatableData: CGFloat + + nonisolated func effectValue(size: CGSize) -> ProjectionTransform { + let translation = amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)) + return ProjectionTransform(CGAffineTransform(translationX: translation, y: 0)) + } + } + + struct WobbleColorView: View { + @State private var wobble = false + + var body: some View { + Color.red.frame(width: 200, height: 200) + .modifier(_OffsetEffect(offset: CGSize(width: 0, height: 100)).ignoredByLayout()) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Example/OpenSwiftUIUITests/Render/GeometryEffect/OffsetEffectUITests.swift b/Example/OpenSwiftUIUITests/Render/GeometryEffect/OffsetEffectUITests.swift new file mode 100644 index 000000000..e58527f07 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Render/GeometryEffect/OffsetEffectUITests.swift @@ -0,0 +1,24 @@ +// +// OffsetEffectUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct OffsetEffectUITests { + @Test + func offsetWithFrame() { + struct ContentView: View { + var body: some View { + Color.blue + .offset(x: 20, y: 15) + .frame(width: 80, height: 60) + .background(Color.red) + .overlay(Color.green.offset(x: 40, y: 30)) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} From 765a14c897cf1b84e77b098b92ff77bfe402030c Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 12 Jul 2025 11:26:52 +0800 Subject: [PATCH 8/8] Fix import issue on Linux --- .../Render/GeometryEffect/IgnoredByLayoutEffect.swift | 2 ++ .../OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift b/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift index 04601704c..8a098ab5f 100644 --- a/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift +++ b/Sources/OpenSwiftUI/Render/GeometryEffect/IgnoredByLayoutEffect.swift @@ -5,6 +5,8 @@ // Audited for 6.5.4 // Status: Complete +public import Foundation + /// A geometry effect type that prevents another geometry effect /// affecting coordinate space conversions during layout, i.e. the /// transform introduced by the other effect is only used when diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift index d495c6b3d..5f03cf8dc 100644 --- a/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/OffsetEffect.swift @@ -5,6 +5,7 @@ // Status: Complete // ID: 72FB21917F353796516DFC9915156779 (SwiftUICore) +import CoreGraphicsShims public import Foundation import OpenGraphShims