diff --git a/Sources/OpenSwiftUICore/Animation/Transition/MoveTransition.swift b/Sources/OpenSwiftUICore/Animation/Transition/MoveTransition.swift new file mode 100644 index 000000000..1f0f60aff --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/Transition/MoveTransition.swift @@ -0,0 +1,110 @@ +// +// MoveTransition.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +import OpenCoreGraphicsShims + +@available(OpenSwiftUI_v1_0, *) +extension AnyTransition { + + /// Returns a transition that moves the view away, towards the specified + /// edge of the view. + public static func move(edge: Edge) -> AnyTransition { + .init(MoveTransition(edge: edge)) + } +} + +@available(OpenSwiftUI_v5_0, *) +extension Transition where Self == MoveTransition { + + /// Returns a transition that moves the view away, towards the specified + /// edge of the view. + @_alwaysEmitIntoClient + @MainActor + @preconcurrency public static func move(edge: Edge) -> Self { + Self(edge: edge) + } +} + +/// Returns a transition that moves the view away, towards the specified +/// edge of the view. +@available(OpenSwiftUI_v5_0, *) +public struct MoveTransition: Transition { + + /// The edge to move the view towards. + public var edge: Edge + + /// Creates a transition that moves the view away, towards the specified + /// edge of the view. + public init(edge: Edge) { + self.edge = edge + } + + public func body( + content: Content, + phase: TransitionPhase + ) -> some View { + content.modifier( + MoveLayout(edge: phase == .identity ? nil : edge) + ) + } + + public func _makeContentTransition(transition: inout _Transition_ContentTransition) { + guard case let .effects(style, size) = transition.operation else { + transition.result = .bool(true) + return + } + let effectiveSize = edge.translationOffset(for: size) + let effect = ContentTransition.Effect(.translation(effectiveSize)) + transition.result = .effects([effect]) + } + + struct MoveLayout: UnaryLayout { + let edge: Edge? + + func placement( + of child: LayoutProxy, + in context: PlacementContext + ) -> _Placement { + let anchorPosition = if let edge { + CGPoint(edge.translationOffset(for: context.size)) + } else { + CGPoint.zero + } + return _Placement( + proposedSize: context.proposedSize, + at: anchorPosition + ) + } + + func sizeThatFits( + in proposedSize: _ProposedSize, + context: SizeAndSpacingContext, + child: LayoutProxy + ) -> CGSize { + child.size(in: proposedSize) + } + } +} + +@available(*, unavailable) +extension MoveTransition: Sendable {} + +extension Edge { + @inline(__always) + fileprivate func translationOffset(for size: CGSize) -> CGSize { + switch self { + case .top: + return CGSize(width: 0, height: -size.height) + case .leading: + return CGSize(width: -size.width, height: 0) + case .bottom: + return CGSize(width: 0, height: size.height) + case .trailing: + return CGSize(width: size.width, height: 0) + } + } +} diff --git a/Sources/OpenSwiftUICore/Animation/Transition/SlideTransition.swift b/Sources/OpenSwiftUICore/Animation/Transition/SlideTransition.swift new file mode 100644 index 000000000..36da95504 --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/Transition/SlideTransition.swift @@ -0,0 +1,60 @@ +// +// SlideTransition.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +@available(OpenSwiftUI_v1_0, *) +extension AnyTransition { + + /// A transition that inserts by moving in from the leading edge, and + /// removes by moving out towards the trailing edge. + /// + /// - SeeAlso: `AnyTransition.move(edge:)` + public static var slide: AnyTransition { + .init(SlideTransition()) + } +} + +@available(OpenSwiftUI_v5_0, *) +extension Transition where Self == SlideTransition { + + /// A transition that inserts by moving in from the leading edge, and + /// removes by moving out towards the trailing edge. + /// + /// - SeeAlso: `AnyTransition.move(edge:)` + @_alwaysEmitIntoClient + @MainActor + @preconcurrency + public static var slide: SlideTransition { + Self() + } +} + +/// A transition that inserts by moving in from the leading edge, and +/// removes by moving out towards the trailing edge. +/// +/// - SeeAlso: `MoveTransition` +@available(OpenSwiftUI_v5_0, *) +public struct SlideTransition: Transition { + + public init() { + _openSwiftUIEmptyStub() + } + + public func body( + content: Content, + phase: TransitionPhase + ) -> some View { + let edge: Edge? = switch phase { + case .willAppear: .leading + case .identity: nil + case .didDisappear: .trailing + } + content.modifier(MoveTransition.MoveLayout(edge: edge)) + } +} + +@available(*, unavailable) +extension SlideTransition: Sendable {}