diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift index b57cb2cf8..c27cc201b 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift @@ -2,7 +2,7 @@ // CoordinateSpace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete /// A resolved coordinate space created by the coordinate space protocol. @@ -10,6 +10,7 @@ /// You don't typically use `CoordinateSpace` directly. Instead, use the static /// properties and functions of `CoordinateSpaceProtocol` such as `.global`, /// `.local`, and `.named(_:)`. +@available(OpenSwiftUI_v1_0, *) public enum CoordinateSpace { /// The global coordinate space at the root of the view hierarchy. case global @@ -21,6 +22,7 @@ public enum CoordinateSpace { case named(AnyHashable) @_spi(ForOpenSwiftUIOnly) + @available(OpenSwiftUI_v6_0, *) public struct ID: Equatable, Sendable { let value: UniqueID @@ -30,6 +32,7 @@ public enum CoordinateSpace { } @_spi(ForOpenSwiftUIOnly) + @available(OpenSwiftUI_v6_0, *) case id(CoordinateSpace.ID) package static let root: CoordinateSpace = .global @@ -51,12 +54,14 @@ public enum CoordinateSpace { @available(*, unavailable) extension CoordinateSpace: Sendable {} +@available(OpenSwiftUI_v1_0, *) extension CoordinateSpace { public var isGlobal: Bool { self == .global } public var isLocal: Bool { self == .local } } +@available(OpenSwiftUI_v1_0, *) extension CoordinateSpace: Equatable, Hashable { public func hash(into hasher: inout Hasher) { switch self { diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift index bff08fc89..7559edee6 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift @@ -2,7 +2,7 @@ // CoordinateSpaceProtocol.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete /// A frame of reference within the layout system. @@ -30,6 +30,7 @@ /// You don't typically create types conforming to this protocol yourself. /// Instead, use the system-provided `.global`, `.local`, and `.named(_:)` /// implementations. +@available(OpenSwiftUI_v5_0, *) public protocol CoordinateSpaceProtocol { /// The resolved coordinate space. var coordinateSpace: CoordinateSpace { get } diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift index 64fce8812..2e4059046 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift @@ -2,10 +2,11 @@ // GlobalCoordinateSpace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete /// The global coordinate space at the root of the view hierarchy. +@available(OpenSwiftUI_v5_0, *) public struct GlobalCoordinateSpace: CoordinateSpaceProtocol { public init() {} @@ -15,6 +16,7 @@ public struct GlobalCoordinateSpace: CoordinateSpaceProtocol { @available(*, unavailable) extension GlobalCoordinateSpace: Sendable {} +@available(OpenSwiftUI_v5_0, *) extension CoordinateSpaceProtocol where Self == GlobalCoordinateSpace { /// The global coordinate space at the root of the view hierarchy. public static var global: GlobalCoordinateSpace { .init() } diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift index 1d8d76320..79f1f9413 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift @@ -2,10 +2,11 @@ // LocalCoordinateSpace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete /// The local coordinate space of the current view. +@available(OpenSwiftUI_v5_0, *) public struct LocalCoordinateSpace: CoordinateSpaceProtocol { public init() {} @@ -15,6 +16,7 @@ public struct LocalCoordinateSpace: CoordinateSpaceProtocol { @available(*, unavailable) extension LocalCoordinateSpace: Sendable {} +@available(OpenSwiftUI_v5_0, *) extension CoordinateSpaceProtocol where Self == LocalCoordinateSpace { /// The local coordinate space of the current view. public static var local: LocalCoordinateSpace { .init() } diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift index 129435ad7..6e954e92b 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift @@ -2,7 +2,7 @@ // NamedCoordinateSpace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete /// A named coordinate space. @@ -10,6 +10,7 @@ /// Use the `coordinateSpace(_:)` modifier to assign a name to the local /// coordinate space of a parent view. Child views can then refer to that /// coordinate space using `.named(_:)`. +@available(OpenSwiftUI_v5_0, *) public struct NamedCoordinateSpace: CoordinateSpaceProtocol, Equatable { package var name: CoordinateSpace.Name @@ -20,9 +21,7 @@ public struct NamedCoordinateSpace: CoordinateSpaceProtocol, Equatable { public var coordinateSpace: CoordinateSpace { name.space } } -@available(*, unavailable) -extension NamedCoordinateSpace: Sendable {} - +@available(OpenSwiftUI_v5_0, *) extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { /// Creates a named coordinate space using the given value. /// @@ -41,3 +40,6 @@ extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { NamedCoordinateSpace(name: .id(id)) } } + +@available(*, unavailable) +extension NamedCoordinateSpace: Sendable {} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift index 9484da861..2cdbc6d80 100644 --- a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift @@ -2,7 +2,7 @@ // ScrollCoordinateSpace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete package enum ScrollCoordinateSpace { @@ -10,32 +10,67 @@ package enum ScrollCoordinateSpace { package static let horizontal: CoordinateSpace.ID = .init() package static let all: CoordinateSpace.ID = .init() package static let content: CoordinateSpace.ID = .init() + package static let safeArea: CoordinateSpace.ID = .init() } extension CoordinateSpace { - package static var verticalScrollView: CoordinateSpace { .id(ScrollCoordinateSpace.vertical) } - package static var horizontalScrollView: CoordinateSpace { .id(ScrollCoordinateSpace.horizontal) } - package static var scrollView: CoordinateSpace { .id(ScrollCoordinateSpace.all) } - package static var scrollViewContent: CoordinateSpace { .id(ScrollCoordinateSpace.content) } + package static var verticalScrollView: CoordinateSpace { + .id(ScrollCoordinateSpace.vertical) + } + + package static var horizontalScrollView: CoordinateSpace { + .id(ScrollCoordinateSpace.horizontal) + } + + package static var scrollView: CoordinateSpace { + .id(ScrollCoordinateSpace.all) + } + + package static var scrollViewContent: CoordinateSpace { + .id(ScrollCoordinateSpace.content) + } + + package static var scrollViewSafeArea: CoordinateSpace { + let id = Semantics.NonScrollableSafeAreaEdges.isEnabled + ? ScrollCoordinateSpace.safeArea + : ScrollCoordinateSpace.content + return .id(id) + } } extension CoordinateSpace.Name { - package static var verticalScrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.vertical) } - package static var horizontalScrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.horizontal) } - package static var scrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.all) } - package static var scrollViewContent: CoordinateSpace.Name { .id(ScrollCoordinateSpace.content) } + package static var verticalScrollView: CoordinateSpace.Name { + .id(ScrollCoordinateSpace.vertical) + } + + package static var horizontalScrollView: CoordinateSpace.Name { + .id(ScrollCoordinateSpace.horizontal) + } + + package static var scrollView: CoordinateSpace.Name { + .id(ScrollCoordinateSpace.all) + } + + package static var scrollViewContent: CoordinateSpace.Name { + .id(ScrollCoordinateSpace.content) + } } +@available(OpenSwiftUI_v5_0, *) extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { /// The named coordinate space that is added by the system for the innermost /// containing scroll view that allows scrolling along the provided axis. public static func scrollView(axis: Axis) -> Self { NamedCoordinateSpace(name: axis == .horizontal ? .horizontalScrollView : .verticalScrollView) } - + /// The named coordinate space that is added by the system for the innermost /// containing scroll view. - public static var scrollView: NamedCoordinateSpace { NamedCoordinateSpace(name: .scrollView) } - - package static var scrollViewContent: NamedCoordinateSpace { NamedCoordinateSpace(name: .scrollViewContent) } + public static var scrollView: NamedCoordinateSpace { + NamedCoordinateSpace(name: .scrollView) + } + + package static var scrollViewContent: NamedCoordinateSpace { + NamedCoordinateSpace(name: .scrollViewContent) + } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift b/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift index b74d29297..f78eb5688 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift @@ -200,7 +200,7 @@ public struct GeometryProxy { guard let placementContext else { return (rect, true) } - let exact = rect.whileClippingToScrollViewsConvert( + let exact = rect.convertAndClipToScrollView( to: space, transform: placementContext.transform ) diff --git a/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift b/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift index c8a2c9d0b..aa0469dbe 100644 --- a/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift +++ b/Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift @@ -2,51 +2,39 @@ // ViewTransform.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete // ID: CE19A3CEA6B9730579C42CE4C3071E74 (SwiftUI) // ID: 1CC2FE016A82CF91549A64E942CE8ED4 (SwiftUICore) package import Foundation -#if !canImport(Darwin) package import OpenCoreGraphicsShims -#endif + +// MARK: - ViewTransform @_spi(ForOpenSwiftUIOnly) public struct ViewTransform: Equatable, CustomStringConvertible { + + // MARK: - ViewTransform.Conversion + package enum Conversion { case rootToSpace(CoordinateSpace) case spaceToRoot(CoordinateSpace) case localToSpace(CoordinateSpace) case spaceToLocal(CoordinateSpace) case spaceToSpace(CoordinateSpace, CoordinateSpace) - + package static func globalToSpace(_ space: CoordinateSpace) -> ViewTransform.Conversion { .spaceToSpace(.global, space) } - + package static func spaceToGlobal(_ space: CoordinateSpace) -> ViewTransform.Conversion { .spaceToSpace(space, .global) } - - // FIXME - @inline(__always) - func normalized() -> Conversion { - guard case let .spaceToSpace(space1, space2) = self else { return self } - if case .local = space1 { - return .localToSpace(space2) - } else if case .local = space2 { - return .spaceToLocal(space1) - } else if .root == space1 { - return .rootToSpace(space2) - } else if .root == space2 { - return .spaceToRoot(space1) - } else { - return self - } - } } - + + // MARK: - ViewTransform.Item + package enum Item: Equatable { case translation(CGSize) case affineTransform(CGAffineTransform, inverse: Bool) @@ -54,104 +42,87 @@ public struct ViewTransform: Equatable, CustomStringConvertible { case coordinateSpace(CoordinateSpace.Name) case sizedSpace(CoordinateSpace.Name, size: CGSize) case scrollGeometry(ViewTransform.ScrollGeometryItem) - - fileprivate func apply(to rect: inout CGRect?, name: CoordinateSpace.Name) { + + fileprivate func apply(to rect: inout CGRect?, coordinateSpaceName: CoordinateSpace.Name) { switch self { - case let .translation(offset): - rect?.origin += offset - case let .affineTransform(matrix, inverse): - #if canImport(CoreGraphics) - guard matrix.isRectilinear else { - rect = nil - break - } - let transform = inverse ? matrix.inverted() : matrix - if var rect { - rect = rect.applying(transform) - } - #else - _openSwiftUIPlatformUnimplementedWarning() - #endif - case .projectionTransform: + case let .translation(offset): + rect?.origin += offset + case let .affineTransform(matrix, inverse): + guard matrix.isRectilinear else { rect = nil - case .coordinateSpace: - break - case let .sizedSpace(spaceName, size): - guard spaceName == name else { break } - rect = CGRect(origin: .zero, size: size) - case .scrollGeometry: break + } + rect = rect?.applying(inverse ? matrix.inverted() : matrix) + case .projectionTransform: + rect = nil + case .coordinateSpace: + break + case let .sizedSpace(spaceName, size): + guard spaceName == coordinateSpaceName else { break } + rect = CGRect(origin: .zero, size: size) + case .scrollGeometry: + break } } - + fileprivate func apply(to geometry: inout ScrollGeometry?, allowUnclipped: Bool) { switch self { - case let .translation(offset): - geometry?.contentOffset += offset - case let .affineTransform(matrix, inverse): - #if canImport(CoreGraphics) - guard matrix.isRectilinear else { - geometry = nil - break - } - let transform = inverse ? matrix.inverted() : matrix - if var geometry { - geometry.contentOffset = geometry.contentOffset.applying(transform) - geometry.containerSize = geometry.containerSize.applying(transform) - } - #else - _openSwiftUIPlatformUnimplementedWarning() - #endif - case .projectionTransform: + case let .translation(offset): + geometry?.contentOffset += offset + case let .affineTransform(matrix, inverse): + guard matrix.isRectilinear else { geometry = nil - case .coordinateSpace: break - case .sizedSpace: + } + if let oldGeometry = geometry { + let transform = inverse ? matrix.inverted() : matrix + geometry!.contentOffset = oldGeometry.contentOffset.applying(transform) + geometry!.containerSize = oldGeometry.containerSize.applying(transform) + } + case .projectionTransform: + geometry = nil + case .coordinateSpace: + break + case .sizedSpace: + break + case let .scrollGeometry(geometryItem): + guard geometryItem.isClipped || allowUnclipped else { break - case let .scrollGeometry(geometryItem): - guard geometryItem.isClipped || allowUnclipped else { - break - } - geometry = geometryItem.base + } + geometry = geometryItem.base } } } - - package struct ScrollGeometryItem: ViewTransformElement { + + // MARK: - ViewTransform.ScrollGeometryItem + + package struct ScrollGeometryItem: Equatable { var base: ScrollGeometry var isClipped: Bool - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.scrollGeometry(self), &stop) - } - - package static func == (lhs: ViewTransform.ScrollGeometryItem, rhs: ViewTransform.ScrollGeometryItem) -> Bool { - lhs.base == rhs.base && lhs.isClipped == rhs.isClipped - } } - + private var head: AnyElement? package private(set) var positionAdjustment: CGSize private var pendingTranslation: CGSize - + package init() { self.head = nil self.positionAdjustment = .zero self.pendingTranslation = .zero } - + package var isEmpty: Bool { head == nil && pendingTranslation == .zero } - + public static func == (lhs: ViewTransform, rhs: ViewTransform) -> Bool { guard lhs.positionAdjustment == rhs.positionAdjustment && lhs.pendingTranslation == rhs.pendingTranslation else { return false } guard let lhsHead = lhs.head, let rhsHead = rhs.head else { return lhs.head == nil && rhs.head == nil } guard lhsHead.depth == rhsHead.depth else { return false } - + var lhsNode: AnyElement? = lhsHead var rhsNode: AnyElement? = rhsHead - + while let lhsElement = lhsNode, let rhsElement = rhsNode { guard lhsElement !== rhsElement else { return true } guard lhsElement.isEqual(to: rhsElement) else { return false } @@ -160,117 +131,146 @@ public struct ViewTransform: Equatable, CustomStringConvertible { } return lhsNode == nil && rhsNode == nil } - + package mutating func append(movingContentsOf elements: inout UnsafeBuffer) { head = BufferedElement(next: head, translation: pendingTranslation, elements: elements) pendingTranslation = .zero elements = UnsafeBuffer() } - + package mutating func appendPosition(_ position: CGPoint) { let adjustedPosition = position - positionAdjustment pendingTranslation = pendingTranslation - CGSize(adjustedPosition) positionAdjustment = CGSize(position) } - + package func withPosition(_ position: CGPoint) -> ViewTransform { var copy = self copy.appendPosition(position) return copy } - + package mutating func appendPosition(_ position: CGPoint, scale: CGFloat) { let adjustedPosition = position - positionAdjustment pendingTranslation = pendingTranslation - CGSize(adjustedPosition) positionAdjustment = CGSize(position) * scale } - + package mutating func resetPosition(_ position: CGPoint) { let adjustedPosition = position - positionAdjustment pendingTranslation = pendingTranslation - CGSize(adjustedPosition) positionAdjustment = .zero } - + package mutating func setPositionAdjustment(_ offset: CGSize) { positionAdjustment = offset } - + package mutating func appendTranslation(_ size: CGSize) { pendingTranslation += size } - + package mutating func appendAffineTransform(_ matrix: CGAffineTransform, inverse: Bool) { if matrix.isTranslation { let translation = CGSize(width: matrix.tx, height: matrix.ty) appendTranslation(inverse ? -translation : translation) } else { - head = Element(next: head, translation: pendingTranslation, element: AffineTransformElement(matrix: matrix, inverse: inverse)) + head = Element( + next: head, + translation: pendingTranslation, + element: AffineTransformElement(matrix: matrix, inverse: inverse) + ) pendingTranslation = .zero } } - + package mutating func appendProjectionTransform(_ matrix: ProjectionTransform, inverse: Bool) { if matrix.isAffine { appendAffineTransform(CGAffineTransform(matrix), inverse: inverse) } else { - head = Element(next: head, translation: pendingTranslation, element: ProjectionTransformElement(matrix: matrix, inverse: inverse)) + head = Element( + next: head, + translation: pendingTranslation, + element: ProjectionTransformElement(matrix: matrix, inverse: inverse) + ) pendingTranslation = .zero } } - + package mutating func appendCoordinateSpace(name: AnyHashable) { - head = Element(next: head, translation: pendingTranslation, element: CoordinateSpaceElement(name: name)) + head = Element( + next: head, + translation: pendingTranslation, + element: CoordinateSpaceElement(name: name) + ) pendingTranslation = .zero } - + package mutating func appendCoordinateSpace(id: CoordinateSpace.ID) { - head = Element(next: head, translation: pendingTranslation, element: CoordinateSpaceIDElement(id: id)) + head = Element( + next: head, + translation: pendingTranslation, + element: CoordinateSpaceIDElement(id: id) + ) pendingTranslation = .zero } - + package mutating func appendSizedSpace(name: AnyHashable, size: CGSize) { - head = Element(next: head, translation: pendingTranslation, element: SizedSpaceElement(name: name, size: size)) + head = Element( + next: head, + translation: pendingTranslation, + element: SizedSpaceElement(name: name, size: size) + ) pendingTranslation = .zero } - + package mutating func appendSizedSpace(id: CoordinateSpace.ID, size: CGSize) { - head = Element(next: head, translation: pendingTranslation, element: SizedSpaceIDElement(id: id, size: size)) + head = Element( + next: head, + translation: pendingTranslation, + element: SizedSpaceIDElement(id: id, size: size) + ) pendingTranslation = .zero } - + package mutating func appendScrollGeometry(_ geometry: ScrollGeometry, isClipped: Bool) { - head = Element(next: head, translation: pendingTranslation, element: ScrollGeometryItem(base: geometry, isClipped: isClipped)) + head = Element( + next: head, + translation: pendingTranslation, + element: ScrollGeometryItem(base: geometry, isClipped: isClipped) + ) pendingTranslation = .zero } - + package func forEach(inverted: Bool, _ body: (Item, inout Bool) -> Void) { guard let head else { return } var stop = false if inverted { if pendingTranslation != .zero { - body(.translation(pendingTranslation), &stop) + body(.translation(-pendingTranslation), &stop) if stop { return } } - var element: AnyElement = head + var current: AnyElement = head repeat { - element.forEach(inverted: true, stop: &stop, body) + let next = current.next + current.forEach(inverted: true, stop: &stop, body) if stop { return } - guard let next = element.next else { break } - element = next + guard let next else { break } + current = next } while true } else { withUnsafeTemporaryAllocation( of: AnyElement.self, capacity: head.depth ) { bufferPointer in - bufferPointer.initializeElement(at: 0, to: head) - var element = head + var current: AnyElement = head var index = 0 - while let next = element.next { - bufferPointer.initializeElement(at: index, to: next) - element = next + repeat { + bufferPointer.initializeElement(at: index, to: current) index &+= 1 - } + guard let next = current.next else { break } + current = next + } while true for element in bufferPointer.reversed() { element.forEach(inverted: false, stop: &stop, body) if stop { return } @@ -281,59 +281,179 @@ public struct ViewTransform: Equatable, CustomStringConvertible { } } } - + package func forEach(_ body: (ViewTransform.Item, inout Bool) -> Void) { forEach(inverted: false, body) } - - private func spaceBeforeSpace(_ space1: CoordinateSpace, _ space2: CoordinateSpace) -> Bool { - if case .global = space1 { - return true - } else if case .local = space1 { - return false - } else if case .global = space2 { - return false - } else if case .local = space2 { - return true - } else { - forEach(inverted: false) { item, stop in - // TODO + + private func spaceBeforeSpace(_ lhsSpace: CoordinateSpace, _ rhsSpace: CoordinateSpace) -> Bool { + let lhsName: CoordinateSpace.Name + let rhsName: CoordinateSpace.Name + switch (lhsSpace, rhsSpace) { + case let (.named(lhs), .named(rhs)): + lhsName = .name(lhs) + rhsName = .name(rhs) + case let (.named(lhs), .id(rhs)): + lhsName = .name(lhs) + rhsName = .id(rhs) + case let (.id(lhs), .named(rhs)): + lhsName = .id(lhs) + rhsName = .name(rhs) + case let (.id(lhs), .id(rhs)): + lhsName = .id(lhs) + rhsName = .id(rhs) + case (.global, _): return true + case (.local, _): return false + case (_, .global): return false + case (_, .local): return true + } + var foundLHSName = false + var foundRHSName = false + forEach(inverted: false) { item, stop in + let name: CoordinateSpace.Name + switch item { + case let .coordinateSpace(itemName): + name = itemName + case let .sizedSpace(itemName, _): + name = itemName + default: + return } - _openSwiftUIUnimplementedFailure() + foundLHSName = name == lhsName + foundRHSName = name == rhsName + stop = foundLHSName || foundRHSName } + return foundLHSName && !foundRHSName } - + package func convert(_ conversion: ViewTransform.Conversion, _ body: (ViewTransform.Item) -> Void) { guard !isEmpty else { return } - _openSwiftUIUnimplementedFailure() + let newConversion: ViewTransform.Conversion + if case let .spaceToSpace(lhsSpace, rhsSpace) = conversion { + if lhsSpace.isLocal { + newConversion = .localToSpace(rhsSpace) + } else if rhsSpace.isLocal { + newConversion = .spaceToLocal(lhsSpace) + } else if lhsSpace.isGlobal { + newConversion = .rootToSpace(rhsSpace) + } else if rhsSpace.isGlobal { + newConversion = .spaceToRoot(lhsSpace) + } else { + newConversion = conversion + } + } else { + newConversion = conversion + } + var isActive = false + switch newConversion { + case let .rootToSpace(space): + guard !space.isGlobal else { return } + isActive = true + case let .spaceToRoot(space): + isActive = space.isLocal + case let .localToSpace(space): + guard !space.isLocal else { return } + isActive = true + case let .spaceToLocal(space): + isActive = space.isGlobal + case .spaceToSpace: + break + } + let inverted: Bool + switch newConversion { + case .rootToSpace, .spaceToLocal: + inverted = false + case .spaceToRoot, .localToSpace: + inverted = true + case let .spaceToSpace(lhsSpace, rhsSpace): + inverted = !spaceBeforeSpace(lhsSpace, rhsSpace) + } + forEach(inverted: inverted) { item, stop in + let itemSpace: CoordinateSpace? + switch item { + case let .coordinateSpace(name): + itemSpace = name.space + case let .sizedSpace(name, _): + itemSpace = name.space + default: + itemSpace = nil + } + if let itemSpace { + switch newConversion { + case let .rootToSpace(space), + let .localToSpace(space): + if itemSpace == space { + stop = true + return + } + case let .spaceToRoot(space), + let .spaceToLocal(space): + if itemSpace == space { + isActive = true + } + case let .spaceToSpace(lhsSpace, rhsSpace): + if itemSpace == rhsSpace { + stop = true + return + } + if itemSpace == lhsSpace { + isActive = true + } + } + } + if isActive { + body(item) + } + } + } + + package func convert(_ conversion: ViewTransform.Conversion, points: inout C) where C: MutableCollection, C.Element == CGPoint { + guard !isEmpty, !points.isEmpty else { return } + convert(conversion) { item in + points._applyTransform(item: item) + } } - + package func convert(_ conversion: ViewTransform.Conversion, points: inout [CGPoint]) { - guard !isEmpty else { return } - _openSwiftUIUnimplementedFailure() + guard !isEmpty, !points.isEmpty else { return } + convert(conversion) { item in + points.applyTransform(item: item) + } } - + package func convert(_ conversion: ViewTransform.Conversion, point: CGPoint) -> CGPoint { guard !isEmpty else { return point } - _openSwiftUIUnimplementedFailure() + var point = point + convert(conversion) { item in + point.applyTransform(item: item) + } + return point } - + package var containingScrollGeometry: ScrollGeometry? { - _openSwiftUIUnimplementedFailure() + var geometry: ScrollGeometry? + forEach { item, _ in + item.apply(to: &geometry, allowUnclipped: false) + } + return geometry } - + package var nearestScrollGeometry: ScrollGeometry? { - _openSwiftUIUnimplementedFailure() + var geometry: ScrollGeometry? + forEach { item, _ in + item.apply(to: &geometry, allowUnclipped: true) + } + return geometry } - + package func containingSizedCoordinateSpace(name: CoordinateSpace.Name) -> CGRect? { var rect: CGRect? forEach { item, _ in - item.apply(to: &rect, name: name) + item.apply(to: &rect, coordinateSpaceName: name) } return rect } - + public var description: String { var descriptionArray = pendingTranslation == .zero ? [] : [String(describing: pendingTranslation)] var element = head @@ -351,69 +471,201 @@ public struct ViewTransform: Equatable, CustomStringConvertible { @available(*, unavailable) extension ViewTransform: Sendable {} +// MARK: - AnyElement + +private class AnyElement { + var next: AnyElement? + let depth: Int + + init(next: AnyElement?) { + self.next = next + if let next { + self.depth = next.depth + 1 + } else { + self.depth = 1 + } + } + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + _openSwiftUIEmptyStub() + } + + func isEqual(to other: AnyElement) -> Bool { + false + } + + var description: String? { nil } +} + +private class Element: AnyElement where Value: ViewTransformElement { + let translation: CGSize + let element: Value + + init(next: AnyElement?, translation: CGSize, element: Value) { + self.translation = translation + self.element = element + super.init(next: next) + } + + override func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + if !inverted, translation != .zero { + body(.translation(translation), &stop) + if stop { return } + } + element.forEach(inverted: inverted, stop: &stop, body) + if stop { return } + if inverted, translation != .zero { + body(.translation(-translation), &stop) + } + } + + override func isEqual(to other: AnyElement) -> Bool { + guard let other = other as? Element else { return false } + return translation == other.translation && element == other.element + } + + override var description: String? { + let description = String(describing: element) + if translation == .zero { + return description + } else { + return "(\(translation), \(description))" + } + } +} + +private class BufferedElement: AnyElement { + let translation: CGSize + var elements: ViewTransform.UnsafeBuffer + + init(next: AnyElement?, translation: CGSize, elements: ViewTransform.UnsafeBuffer) { + self.translation = translation + self.elements = elements + super.init(next: next) + } + + deinit { + elements.destroy() + } + + override func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + if !inverted, translation != .zero { + body(.translation(translation), &stop) + if stop { return } + } + elements.forEach(inverted: inverted, stop: &stop, body) + if stop { return } + if inverted, translation != .zero { + body(.translation(-translation), &stop) + } + } + + override func isEqual(to other: AnyElement) -> Bool { + guard let other = other as? BufferedElement else { return false } + return translation == other.translation && elements == other.elements + } + + override var description: String? { + let description = elements.description + if translation == .zero { + return description + } else { + return "(\(translation), \(description))" + } + } +} + +// MARK: - ViewTransform.UnsafeBuffer + @_spi(ForOpenSwiftUIOnly) extension ViewTransform { package struct UnsafeBuffer: Equatable { var contents: UnsafeHeterogeneousBuffer - + typealias Element = UnsafeHeterogeneousBuffer.Element - + package init() { contents = .init() } - + package mutating func destroy() { contents.destroy() } - + package mutating func appendTranslation(_ size: CGSize) { guard size != .zero else { return } - contents.append(TranslationElement(offset: size), vtable: _VTable.self) + contents.append( + TranslationElement(offset: size), + vtable: _VTable.self + ) } - + package mutating func appendAffineTransform(_ matrix: CGAffineTransform, inverse: Bool) { if matrix.isTranslation { let tranlation = CGSize(width: matrix.tx, height: matrix.ty) appendTranslation(inverse ? -tranlation : tranlation) } else { - contents.append(AffineTransformElement(matrix: matrix, inverse: inverse), vtable: _VTable.self) + contents.append( + AffineTransformElement(matrix: matrix, inverse: inverse), + vtable: _VTable.self + ) } } - + package mutating func appendProjectionTransform(_ matrix: ProjectionTransform, inverse: Bool) { if matrix.isAffine { appendAffineTransform(CGAffineTransform(matrix), inverse: inverse) } else { - contents.append(ProjectionTransformElement(matrix: matrix, inverse: inverse), vtable: _VTable.self) + contents.append( + ProjectionTransformElement(matrix: matrix, inverse: inverse), + vtable: _VTable.self + ) } } - + package mutating func appendCoordinateSpace(id: CoordinateSpace.ID) { - contents.append(CoordinateSpaceIDElement(id: id), vtable: _VTable.self) + contents.append( + CoordinateSpaceIDElement(id: id), + vtable: _VTable.self + ) } - + package mutating func appendSizedSpace(id: CoordinateSpace.ID, size: CGSize) { - contents.append(SizedSpaceIDElement(id: id, size: size), vtable: _VTable.self) + contents.append( + SizedSpaceIDElement(id: id, size: size), + vtable: _VTable.self + ) } - + package mutating func appendScrollGeometry(_ geometry: ScrollGeometry, isClipped: Bool) { - contents.append(ScrollGeometryItem(base: geometry, isClipped: isClipped), vtable: _VTable.self) - } - - package static func == (lhs: ViewTransform.UnsafeBuffer, rhs: ViewTransform.UnsafeBuffer) -> Bool { - guard lhs.contents.count == rhs.contents.count else { return false } - guard lhs.contents.count > 0 else { return true } - for index in lhs.contents.indices { - let lhsElement = lhs.contents[index] - let rhsElement = rhs.contents[index] + contents.append( + ScrollGeometryItem(base: geometry, isClipped: isClipped), + vtable: _VTable.self + ) + } + + package static func == ( + lhs: ViewTransform.UnsafeBuffer, + rhs: ViewTransform.UnsafeBuffer + ) -> Bool { + let lhsCount = lhs.contents.count + let rhsCount = rhs.contents.count + guard lhsCount == rhsCount else { return false } + for (lhsElement, rhsElement) in zip(lhs.contents, rhs.contents) { guard lhsElement.item.pointee.vtable == rhsElement.item.pointee.vtable, - lhsElement.vtable(as: VTable.self).equal(lhsElement, rhsElement) - else { return false } + lhsElement.vtable(as: VTable.self).equal(lhsElement, rhsElement) else { + return false + } + continue } return true } - - fileprivate func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + + fileprivate func forEach( + inverted: Bool, + stop: inout Bool, + _ body: (ViewTransform.Item, inout Bool) -> () + ) { if inverted { withUnsafeTemporaryAllocation( of: UnsafeBuffer.Element.self, @@ -423,57 +675,94 @@ extension ViewTransform { bufferPointer.initializeElement(at: index, to: element) } for element in bufferPointer.reversed() { - element.vtable(as: VTable.self).forEach(elt: element, inverted: true, stop: &stop, body) + element.vtable(as: VTable.self) + .forEach(elt: element, inverted: true, stop: &stop, body) if stop { return } } } } else { for element in contents { - element.vtable(as: VTable.self).forEach(elt: element, inverted: false, stop: &stop, body) + element.vtable(as: VTable.self) + .forEach(elt: element, inverted: false, stop: &stop, body) if stop { return } } } } - + fileprivate var description: String { let contentsDescription = contents.map { element in element.vtable(as: VTable.self).description(elt: element) } return "[\(contentsDescription.joined(separator: ", "))]" } - + // MARK: - ViewTransform.UnsafeBuffer.VTable - + private class VTable: _UnsafeHeterogeneousBuffer_VTable { - class func forEach(elt: _UnsafeHeterogeneousBuffer_Element, inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) {} - class func description(elt: _UnsafeHeterogeneousBuffer_Element) -> String { "" } - class func equal(_ lhs: _UnsafeHeterogeneousBuffer_Element, _ rhs: _UnsafeHeterogeneousBuffer_Element) -> Bool { false } + class func forEach( + elt: _UnsafeHeterogeneousBuffer_Element, + inverted: Bool, + stop: inout Bool, + _ body: (ViewTransform.Item, inout Bool) -> () + ) { + _openSwiftUIEmptyStub() + } + + class func description( + elt: _UnsafeHeterogeneousBuffer_Element + ) -> String { + "" + } + + class func equal( + _ lhs: _UnsafeHeterogeneousBuffer_Element, + _ rhs: _UnsafeHeterogeneousBuffer_Element + ) -> Bool { + false + } } - + private final class _VTable: VTable where Element: ViewTransformElement { override class func hasType(_ type: T.Type) -> Bool { Element.self == type } - - override class func moveInitialize(elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element) { + + override class func moveInitialize( + elt: _UnsafeHeterogeneousBuffer_Element, + from: _UnsafeHeterogeneousBuffer_Element + ) { let dest = elt.body(as: Element.self) let source = from.body(as: Element.self) dest.initialize(to: source.move()) } - - override class func deinitialize(elt: UnsafeHeterogeneousBuffer.Element) { - elt.body(as: Element.self).deinitialize(count: 1) + + override class func deinitialize( + elt: UnsafeHeterogeneousBuffer.Element + ) { + elt.body(as: Element.self) + .deinitialize(count: 1) } - - override class func forEach(elt: _UnsafeHeterogeneousBuffer_Element, inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - elt.body(as: Element.self).pointee.forEach(inverted: inverted, stop: &stop, body) + + override class func forEach( + elt: _UnsafeHeterogeneousBuffer_Element, + inverted: Bool, + stop: inout Bool, + _ body: (ViewTransform.Item, inout Bool) -> () + ) { + elt.body(as: Element.self) + .pointee.forEach(inverted: inverted, stop: &stop, body) } - - override class func description(elt: _UnsafeHeterogeneousBuffer_Element) -> String { + + override class func description( + elt: _UnsafeHeterogeneousBuffer_Element + ) -> String { String(describing: elt.body(as: Element.self).pointee) } - - override class func equal(_ lhs: _UnsafeHeterogeneousBuffer_Element, _ rhs: _UnsafeHeterogeneousBuffer_Element) -> Bool { + + override class func equal( + _ lhs: _UnsafeHeterogeneousBuffer_Element, + _ rhs: _UnsafeHeterogeneousBuffer_Element + ) -> Bool { let lhs = lhs.body(as: Element.self) let rhs = rhs.body(as: Element.self) return lhs.pointee == rhs.pointee @@ -482,194 +771,34 @@ extension ViewTransform { } } -// MARK: - ViewTransformElement - -private protocol ViewTransformElement: Equatable { - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) -} - -private struct TranslationElement: ViewTransformElement { - var offset: CGSize - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.translation(inverted ? -offset : offset), &stop) - } -} - -private struct AffineTransformElement: ViewTransformElement { - var matrix: CGAffineTransform - var inverse: Bool - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.affineTransform(matrix, inverse: inverse), &stop) - } -} - -private struct ProjectionTransformElement: ViewTransformElement { - var matrix: ProjectionTransform - var inverse: Bool - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.projectionTransform(matrix, inverse: inverse), &stop) - } -} - -private struct CoordinateSpaceElement: ViewTransformElement { - var name: AnyHashable - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.coordinateSpace(.name(name)), &stop) - } -} - -private struct CoordinateSpaceIDElement: ViewTransformElement { - var id: CoordinateSpace.ID - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.coordinateSpace(.id(id)), &stop) - } -} - -private struct SizedSpaceElement: ViewTransformElement { - var name: AnyHashable - var size: CGSize - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.sizedSpace(.name(name), size: size), &stop) - } -} - -private struct SizedSpaceIDElement: ViewTransformElement { - var id: CoordinateSpace.ID - var size: CGSize - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - body(.sizedSpace(.id(id), size: size), &stop) - } -} - -// MARK: - AnyElement - -private class AnyElement { - var next: AnyElement? - let depth: Int - - init(next: AnyElement?) { - self.next = next - self.depth = (next?.depth ?? 0) + 1 - } - - func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) {} - func isEqual(to other: AnyElement) -> Bool { false } - var description: String? { nil } -} - -private class Element: AnyElement where Value: ViewTransformElement { - let translation: CGSize - let element: Value - - init(next: AnyElement?, translation: CGSize, element: Value) { - self.translation = translation - self.element = element - super.init(next: next) - } - - override func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - if !inverted, translation != .zero { - body(.translation(translation), &stop) - if stop { return } - } - element.forEach(inverted: inverted, stop: &stop, body) - if stop { return } - if inverted, translation != .zero { - body(.translation(-translation), &stop) - } - } - - override func isEqual(to other: AnyElement) -> Bool { - guard let otherElement = other as? Element else { return false } - return translation == otherElement.translation && element == otherElement.element - } - - override var description: String? { - let description = String(describing: element) - if translation == .zero { - return description - } else { - return "(\(translation), \(description))" - } - } -} - -private class BufferedElement: AnyElement { - let translation: CGSize - var elements: ViewTransform.UnsafeBuffer - - init(next: AnyElement?, translation: CGSize, elements: ViewTransform.UnsafeBuffer) { - self.translation = translation - self.elements = elements - super.init(next: next) - } - - override func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { - if !inverted, translation != .zero { - body(.translation(translation), &stop) - if stop { return } - } - elements.forEach(inverted: inverted, stop: &stop, body) - if stop { return } - if inverted, translation != .zero { - body(.translation(-translation), &stop) - } - } - - override func isEqual(to other: AnyElement) -> Bool { - guard let otherElement = other as? BufferedElement else { return false } - return translation == otherElement.translation && elements == otherElement.elements - } - - override var description: String? { - let description = elements.description - if translation == .zero { - return description - } else { - return "(\(translation), \(description))" - } - } -} - -// MARK: - ViewTransformable +// MARK: - ApplyViewTransform private protocol ApplyViewTransform { mutating func applyTransform(item: ViewTransform.Item) } extension ApplyViewTransform { - mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { + package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { transform.convert(.localToSpace(space)) { item in applyTransform(item: item) } } + + package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { + transform.convert(.spaceToLocal(space)) { item in + applyTransform(item: item) + } + } } +// MARK: - ViewTransformable + package protocol ViewTransformable { mutating func convert(to space: CoordinateSpace, transform: ViewTransform) mutating func convert(from space: CoordinateSpace, transform: ViewTransform) } -extension ViewTransformable where Self: ApplyViewTransform { - mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { - transform.convert(.localToSpace(space)) { item in - applyTransform(item: item) - } - } - - mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { - transform.convert(.spaceToLocal(space)) { item in - applyTransform(item: item) - } - } -} +// MARK: - CGPoint + ApplyViewTransform & ViewTransformable extension CGPoint: ApplyViewTransform, ViewTransformable { package mutating func applyTransform(item: ViewTransform.Item) { @@ -677,7 +806,6 @@ extension CGPoint: ApplyViewTransform, ViewTransformable { case let .translation(offset): self += offset case let .affineTransform(matrix, inverse): - #if canImport(CoreGraphics) if inverse { if matrix.isTranslation { self -= CGSize(width: matrix.tx, height: matrix.ty) @@ -687,86 +815,174 @@ extension CGPoint: ApplyViewTransform, ViewTransformable { } else { self = applying(matrix) } - #else - _openSwiftUIPlatformUnimplementedWarning() - #endif case let .projectionTransform(matrix, inverse): self = inverse ? unapplying(matrix) : applying(matrix) case .coordinateSpace, .sizedSpace, .scrollGeometry: break } } - - package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { - self = transform.convert(.localToSpace(space), point: self) +} + +extension MutableCollection where Element == CGPoint { + fileprivate mutating func _applyTransform(item: ViewTransform.Item) { + switch item { + case let .translation(offset): + for index in indices { + self[index] += offset + } + case let .affineTransform(matrix, inverse): + let transform = inverse ? matrix.inverted() : matrix + for index in indices { + self[index] = self[index].applying(transform) + } + case let .projectionTransform(matrix, inverse): + _apply(matrix, inverse: inverse) + case .coordinateSpace, .sizedSpace, .scrollGeometry: + break + } } - - package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { - self = transform.convert(.spaceToLocal(space), point: self) + + fileprivate mutating func _apply(_ transform: ProjectionTransform, inverse: Bool) { + for index in indices { + self[index] = inverse ? self[index].unapplying(transform) : self[index].applying(transform) + } } } extension [CGPoint]: ApplyViewTransform, ViewTransformable { package mutating func applyTransform(item: ViewTransform.Item) { - switch item { - case let .translation(offset): - self = map { $0 + offset } - case let .affineTransform(matrix, inverse): - #if canImport(CoreGraphics) - let tranform = inverse ? matrix.inverted() : matrix - self = map { $0.applying(tranform) } - #else - _openSwiftUIPlatformUnimplementedWarning() - #endif - case let .projectionTransform(matrix, inverse): - apply(matrix, inverse: inverse) - case .coordinateSpace, .sizedSpace, .scrollGeometry: - break - } + _applyTransform(item: item) } - + package mutating func apply(_ m: ProjectionTransform, inverse: Bool) { - self = map { inverse ? $0.unapplying(m) : $0.applying(m) } - } - - package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { - transform.convert(.localToSpace(space), points: &self) - } - - package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { - transform.convert(.spaceToLocal(space), points: &self) + _apply(m, inverse: inverse) } } +// MARK: - CGRect + ViewTransformable + extension CGRect: ViewTransformable { package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { - guard !isNull else { return } - guard !isInfinite else { return } + guard !isNull, !isInfinite else { return } var points = cornerPoints points.convert(to: space, transform: transform) self = CGRect(cornerPoints: points) } - + package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { - guard !isNull else { return } - guard !isInfinite else { return } + guard !isNull, !isInfinite else { return } var points = cornerPoints points.convert(from: space, transform: transform) self = CGRect(cornerPoints: points) } - - package mutating func whileClippingToScrollViewsConvert(to space: CoordinateSpace, transform: ViewTransform) -> Bool { - guard !isNull else { return true } - guard !isInfinite else { return true } + + package mutating func convertAndClipToScrollView(to space: CoordinateSpace, transform: ViewTransform) -> Bool { + guard !isNull, !isInfinite else { return true } + var isNonRectilinear = false transform.convert(.localToSpace(space)) { item in - // TODO + switch item { + case let .translation(offset): + origin += offset + case let .affineTransform(matrix, inverse): + self = applying(inverse ? matrix.inverted() : matrix) + isNonRectilinear = isNonRectilinear || !matrix.isRectilinear + case let .projectionTransform(matrix, inverse): + if !isNull && !isInfinite { + var points = cornerPoints + points.apply(matrix, inverse: inverse) + self = CGRect(cornerPoints: points) + } + isNonRectilinear = true + case .coordinateSpace, .sizedSpace: + break + case let .scrollGeometry(geometry): + self = intersection(CGRect(origin: .zero, size: geometry.base.containerSize)) + } } - _openSwiftUIUnimplementedFailure() + return !isNonRectilinear } } -// TODO: Path + ViewTransformable -//extension Path: ViewTransformable { -// package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) -// package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) -//} +// MARK: - Path + ViewTransformable + +extension Path: ViewTransformable { + package mutating func convert(to space: CoordinateSpace, transform: ViewTransform) { + mapPoints { $0.convert(to: space, transform: transform) } + } + + package mutating func convert(from space: CoordinateSpace, transform: ViewTransform) { + mapPoints { $0.convert(from: space, transform: transform) } + } +} + +// MARK: - ViewTransformElement + +private protocol ViewTransformElement: Equatable { + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) +} + +private struct TranslationElement: ViewTransformElement { + var offset: CGSize + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.translation(inverted ? -offset : offset), &stop) + } +} + +private struct SizedSpaceIDElement: ViewTransformElement { + var id: CoordinateSpace.ID + var size: CGSize + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.sizedSpace(.id(id), size: size), &stop) + } +} + +private struct SizedSpaceElement: ViewTransformElement { + var name: AnyHashable + var size: CGSize + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.sizedSpace(.name(name), size: size), &stop) + } +} + +private struct CoordinateSpaceIDElement: ViewTransformElement { + var id: CoordinateSpace.ID + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.coordinateSpace(.id(id)), &stop) + } +} + +private struct AffineTransformElement: ViewTransformElement { + var matrix: CGAffineTransform + var inverse: Bool + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.affineTransform(matrix, inverse: inverse != inverted), &stop) + } +} + +private struct CoordinateSpaceElement: ViewTransformElement { + var name: AnyHashable + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.coordinateSpace(.name(name)), &stop) + } +} + +private struct ProjectionTransformElement: ViewTransformElement { + var matrix: ProjectionTransform + var inverse: Bool + + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.projectionTransform(matrix, inverse: inverse != inverted), &stop) + } +} + +extension ViewTransform.ScrollGeometryItem: ViewTransformElement { + func forEach(inverted: Bool, stop: inout Bool, _ body: (ViewTransform.Item, inout Bool) -> ()) { + body(.scrollGeometry(self), &stop) + } +} diff --git a/Sources/OpenSwiftUICore/Shape/Path.swift b/Sources/OpenSwiftUICore/Shape/Path.swift index 459a44526..9a69d8230 100644 --- a/Sources/OpenSwiftUICore/Shape/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path.swift @@ -843,6 +843,12 @@ extension Path { ) -> Path { _openSwiftUIUnimplementedFailure() } + + func mapPoints(_ body: (inout [CGPoint]) -> ()) -> Path { + // TODO + _openSwiftUIUnimplementedWarning() + return self + } } // MARK: - RenderBox diff --git a/Tests/OpenSwiftUICoreTests/Layout/View/ViewTransformTests.swift b/Tests/OpenSwiftUICoreTests/Layout/View/ViewTransformTests.swift index 24faf4087..ade0ccc83 100644 --- a/Tests/OpenSwiftUICoreTests/Layout/View/ViewTransformTests.swift +++ b/Tests/OpenSwiftUICoreTests/Layout/View/ViewTransformTests.swift @@ -63,4 +63,176 @@ struct ViewTransformTests { ((10.0, 10.0), CoordinateSpaceElement(name: AnyHashable("a"))); SizedSpaceElement(name: AnyHashable("b"), size: (20.0, 20.0)) """#) } + + @Test + func convertSpaceToLocal() { + let space = CoordinateSpace.ID() + var transform = ViewTransform() + transform.appendTranslation(CGSize(width: 10, height: 20)) + transform.appendSizedSpace(id: space, size: CGSize(width: 100, height: 200)) + transform.appendTranslation(CGSize(width: 3, height: 4)) + + var items: [ViewTransform.Item] = [] + transform.convert(.spaceToLocal(.id(space))) { item in + items.append(item) + } + + #expect(items == [ + .sizedSpace(.id(space), size: CGSize(width: 100, height: 200)), + .translation(CGSize(width: 3, height: 4)), + ]) + } + + @Test + func convertBetweenSpaces() { + let parentSpace = CoordinateSpace.ID() + let childSpace = CoordinateSpace.ID() + var transform = ViewTransform() + transform.appendSizedSpace(id: parentSpace, size: CGSize(width: 100, height: 200)) + transform.appendTranslation(CGSize(width: 3, height: 4)) + transform.appendCoordinateSpace(id: childSpace) + + var forwardItems: [ViewTransform.Item] = [] + transform.convert(.spaceToSpace(.id(parentSpace), .id(childSpace))) { item in + forwardItems.append(item) + } + #expect(forwardItems == [ + .sizedSpace(.id(parentSpace), size: CGSize(width: 100, height: 200)), + .translation(CGSize(width: 3, height: 4)), + ]) + + var inverseItems: [ViewTransform.Item] = [] + transform.convert(.spaceToSpace(.id(childSpace), .id(parentSpace))) { item in + inverseItems.append(item) + } + #expect(inverseItems == [ + .coordinateSpace(.id(childSpace)), + .translation(CGSize(width: -3, height: -4)), + ]) + } + + @Test + func inverseConversionInvertsTransformItems() { + let affine = CGAffineTransform(scaleX: 2, y: 3) + let projection = ProjectionTransform( + m11: 1, m12: 0, m13: 0.1, + m21: 0, m22: 1, m23: 0, + m31: 5, m32: 7, m33: 1 + ) + var transform = ViewTransform() + transform.appendAffineTransform(affine, inverse: false) + transform.appendProjectionTransform(projection, inverse: true) + + var items: [ViewTransform.Item] = [] + transform.convert(.localToSpace(.root)) { item in + items.append(item) + } + + #expect(items == [ + .projectionTransform(projection, inverse: false), + .affineTransform(affine, inverse: true), + ]) + } + + @Test + func convertsPointsAndMutableCollections() { + let space = CoordinateSpace.ID() + var transform = ViewTransform() + transform.appendSizedSpace(id: space, size: CGSize(width: 100, height: 200)) + transform.appendTranslation(CGSize(width: 3, height: 4)) + + let convertedPoint = transform.convert( + .spaceToLocal(.id(space)), + point: CGPoint(x: 1, y: 2) + ) + #expect(convertedPoint == CGPoint(x: 4, y: 6)) + #expect(transform.convert(.localToSpace(.id(space)), point: convertedPoint) == CGPoint(x: 1, y: 2)) + + var points = [CGPoint(x: 1, y: 2), CGPoint(x: 5, y: 6)] + transform.convert(.spaceToLocal(.id(space)), points: &points) + #expect(points == [CGPoint(x: 4, y: 6), CGPoint(x: 8, y: 10)]) + + var slice = points[...] + transform.convert(.localToSpace(.id(space)), points: &slice) + #expect(Array(slice) == [CGPoint(x: 1, y: 2), CGPoint(x: 5, y: 6)]) + } + + @Test + func containingSizedCoordinateSpaceAppliesAffineTransforms() { + let name = CoordinateSpace.Name.name(AnyHashable("target")) + var transform = ViewTransform() + transform.appendSizedSpace(name: "target", size: CGSize(width: 10, height: 20)) + transform.appendAffineTransform(CGAffineTransform(scaleX: 2, y: 3), inverse: false) + + #expect( + transform.containingSizedCoordinateSpace(name: name) == + CGRect(origin: .zero, size: CGSize(width: 20, height: 60)) + ) + } + + @Test + func scrollGeometryQueriesRespectClippingAndTransforms() { + let geometry = ScrollGeometry( + contentOffset: CGPoint(x: 1, y: 2), + contentSize: CGSize(width: 100, height: 200), + contentInsets: .zero, + containerSize: CGSize(width: 20, height: 30) + ) + + var unclippedTransform = ViewTransform() + unclippedTransform.appendScrollGeometry(geometry, isClipped: false) + unclippedTransform.appendTranslation(CGSize(width: 3, height: 4)) + + var translatedGeometry = geometry + translatedGeometry.contentOffset += CGSize(width: 3, height: 4) + #expect(unclippedTransform.containingScrollGeometry == nil) + #expect(unclippedTransform.nearestScrollGeometry == translatedGeometry) + + var clippedTransform = ViewTransform() + clippedTransform.appendScrollGeometry(geometry, isClipped: true) + clippedTransform.appendAffineTransform(CGAffineTransform(scaleX: 2, y: 3), inverse: false) + + var transformedGeometry = geometry + transformedGeometry.contentOffset = CGPoint(x: 2, y: 6) + transformedGeometry.containerSize = CGSize(width: 40, height: 90) + #expect(clippedTransform.containingScrollGeometry == transformedGeometry) + #expect(clippedTransform.nearestScrollGeometry == transformedGeometry) + } + + @Test + func convertAndClipToScrollView() { + let space = CoordinateSpace.ID() + let geometry = ScrollGeometry( + contentOffset: .zero, + contentSize: CGSize(width: 100, height: 100), + contentInsets: .zero, + containerSize: CGSize(width: 50, height: 60) + ) + var transform = ViewTransform() + transform.appendCoordinateSpace(id: space) + transform.appendScrollGeometry(geometry, isClipped: false) + + var rect = CGRect(x: -10, y: -10, width: 100, height: 100) + let didConvertRect = rect.convertAndClipToScrollView(to: .id(space), transform: transform) + #expect(didConvertRect) + #expect(rect == CGRect(x: 0, y: 0, width: 50, height: 60)) + + var nonRectilinearTransform = ViewTransform() + nonRectilinearTransform.appendCoordinateSpace(id: space) + nonRectilinearTransform.appendAffineTransform( + CGAffineTransform(rotationAngle: .pi / 4), + inverse: false + ) + var rotatedRect = CGRect(x: 0, y: 0, width: 10, height: 20) + let didConvertRotatedRect = rotatedRect.convertAndClipToScrollView( + to: .id(space), + transform: nonRectilinearTransform + ) + #expect(!didConvertRotatedRect) + + var nullRect = CGRect.null + let didConvertNullRect = nullRect.convertAndClipToScrollView(to: .id(space), transform: transform) + #expect(didConvertNullRect) + #expect(nullRect.isNull) + } }