diff --git a/Example/HostingExample/ViewController.swift b/Example/HostingExample/ViewController.swift index c500e4ba6..491712995 100644 --- a/Example/HostingExample/ViewController.swift +++ b/Example/HostingExample/ViewController.swift @@ -66,6 +66,6 @@ class ViewController: NSViewController { struct ContentView: View { var body: some View { - SpringAnimationExample() + ColorRepresentableExample() } } diff --git a/Example/OpenSwiftUIUITests/Integration/Representable/PlatformViewRepresentableUITests.swift b/Example/OpenSwiftUIUITests/Integration/Representable/PlatformViewRepresentableUITests.swift new file mode 100644 index 000000000..7321ba570 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Integration/Representable/PlatformViewRepresentableUITests.swift @@ -0,0 +1,48 @@ +// +// PlatformViewRepresentableUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +#if os(iOS) +import UIKit +typealias PlatformViewRepresentable = UIViewRepresentable +#elseif os(macOS) +import AppKit +typealias PlatformViewRepresentable = NSViewRepresentable +#endif + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct PlatformViewRepresentableUITests { + @Test + func plainColorView() { + struct PlainColorView: PlatformViewRepresentable { + #if os(iOS) + func makeUIView(context: Context) -> some UIView { + let v = UIView() + v.backgroundColor = .red + return v + } + + func updateUIView(_ uiView: UIViewType, context: Context) {} + #elseif os(macOS) + func makeNSView(context: Context) -> some NSView { + let v = NSView() + v.wantsLayer = true + v.layer?.backgroundColor = NSColor.red.cgColor + return v + } + + func updateNSView(_ uiView: NSViewType, context: Context) {} + #endif + } + struct ContentView: View { + var body: some View { + PlainColorView() + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Example/SharedExample/Integration/Representable/ColorRepresentableExample.swift b/Example/SharedExample/Integration/Representable/ColorRepresentableExample.swift new file mode 100644 index 000000000..c04c94566 --- /dev/null +++ b/Example/SharedExample/Integration/Representable/ColorRepresentableExample.swift @@ -0,0 +1,38 @@ +// +// ColorRepresentable.swift +// SharedExample + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +#if os(iOS) +import UIKit +typealias PlatformViewRepresentable = UIViewRepresentable +#elseif os(macOS) +import AppKit +typealias PlatformViewRepresentable = NSViewRepresentable +#endif + +struct ColorRepresentableExample: PlatformViewRepresentable { + #if os(iOS) + func makeUIView(context: Context) -> some UIView { + let v = UIView() + v.backgroundColor = .red + return v + } + + func updateUIView(_ uiView: UIViewType, context: Context) {} + #elseif os(macOS) + func makeNSView(context: Context) -> some NSView { + let v = NSView() + v.wantsLayer = true + v.layer?.backgroundColor = NSColor.red.cgColor + return v + } + + func updateNSView(_ uiView: NSViewType, context: Context) {} + #endif +} diff --git a/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift b/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift index d5a2e3c34..18f0e14d4 100644 --- a/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift +++ b/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift @@ -6,6 +6,7 @@ // Status: WIP import OpenSwiftUICore +import OpenGraphShims @available(OpenSwiftUI_v2_0, *) @propertyWrapper @@ -99,3 +100,10 @@ extension FocusedValues: Equatable { lhs.seed.matches(rhs.seed) } } + +// FIXME +struct FocusedValuesInputKey: ViewInput { + static var defaultValue: OptionalAttribute { + .init() + } +} diff --git a/Sources/OpenSwiftUI/Integration/Render/AppKit/NSViewPlatformViewDefinition.swift b/Sources/OpenSwiftUI/Integration/Render/AppKit/NSViewPlatformViewDefinition.swift index 44aa1a8c4..741293c84 100644 --- a/Sources/OpenSwiftUI/Integration/Render/AppKit/NSViewPlatformViewDefinition.swift +++ b/Sources/OpenSwiftUI/Integration/Render/AppKit/NSViewPlatformViewDefinition.swift @@ -59,5 +59,17 @@ final class NSViewPlatformViewDefinition: PlatformViewDefinition, @unchecked Sen override static func makeLayerView(type: CALayer.Type, kind: PlatformViewDefinition.ViewKind) -> AnyObject { _openSwiftUIUnimplementedFailure() } + + override class func makePlatformView(view: AnyObject, kind: PlatformViewDefinition.ViewKind) { + Self.initView(view as! NSView, kind: kind) + } + + override class func setAllowsWindowActivationEvents(_ value: Bool?, for view: AnyObject) { + _openSwiftUIUnimplementedWarning() + } + + override class func setHitTestsAsOpaque(_ value: Bool, for view: AnyObject) { + _openSwiftUIUnimplementedWarning() + } } #endif diff --git a/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift b/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift index f58f82cf2..124b9ec1e 100644 --- a/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift +++ b/Sources/OpenSwiftUI/Integration/Render/UIKit/UIViewPlatformViewDefinition.swift @@ -54,5 +54,9 @@ final class UIViewPlatformViewDefinition: PlatformViewDefinition, @unchecked Sen break } } + + override class func makePlatformView(view: AnyObject, kind: PlatformViewDefinition.ViewKind) { + Self.initView(view as! UIView, kind: kind) + } } #endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewControllerRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewControllerRepresentable.swift new file mode 100644 index 000000000..248f3460f --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewControllerRepresentable.swift @@ -0,0 +1,6 @@ +// +// NSViewControllerRepresentable.swift +// OpenSwiftUI + +// TODO +package struct NSViewControllerRepresentable {} diff --git a/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewRepresentable.swift new file mode 100644 index 000000000..b3ad768e0 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/AppKit/NSViewRepresentable.swift @@ -0,0 +1,503 @@ +// +// NSViewRepresentable.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: 38FE679A85C91B802D25DB73BF37B09F (SwiftUI) + +#if os(macOS) + +public import AppKit +public import OpenSwiftUICore +import OpenGraphShims + +// MARK: - NSViewRepresentable + +/// A wrapper that you use to integrate an AppKit view into your SwiftUI view +/// hierarchy. +/// +/// Use an `NSViewRepresentable` instance to create and manage an +/// [NSView](https://developer.apple.com/documentation/appkit/nsview) object in your OpenSwiftUI +/// interface. Adopt this protocol in one of your app's custom instances, and +/// use its methods to create, update, and tear down your view. The creation and +/// update processes parallel the behavior of OpenSwiftUI views, and you use them to +/// configure your view with your app's current state information. Use the +/// teardown process to remove your view cleanly from your OpenSwiftUI. For example, +/// you might use the teardown process to notify other objects that the view is +/// disappearing. +/// +/// To add your view into your OpenSwiftUI interface, create your +/// ``NSViewRepresentable`` instance and add it to your OpenSwiftUI interface. The +/// system calls the methods of your representable instance at appropriate times +/// to create and update the view. The following example shows the inclusion of +/// a custom `MyRepresentedCustomView` struct in the view hierarchy. +/// +/// struct ContentView: View { +/// var body: some View { +/// VStack { +/// Text("Global Sales") +/// MyRepresentedCustomView() +/// } +/// } +/// } +/// +/// The system doesn't automatically communicate changes occurring within your +/// view controller to other parts of your OpenSwiftUI interface. When you want your +/// view controller to coordinate with other OpenSwiftUI views, you must provide a +/// ``NSViewControllerRepresentable/Coordinator`` object to facilitate those +/// interactions. For example, you use a coordinator to forward target-action +/// and delegate messages from your view controller to any OpenSwiftUI views. +/// +/// - Warning: OpenSwiftUI fully controls the layout of the AppKit view using the view's +/// [frame](https://developer.apple.com/documentation/appkit/nsview/1483713-frame) and +/// [bounds](https://developer.apple.com/documentation/appkit/nsview/1483817-bounds) +/// properties. Don't directly set these layout-related properties on the view +/// managed by an `NSViewRepresentable` instance from your own +/// code because that conflicts with OpenSwiftUI and results in undefined behavior. +@available(OpenSwiftUI_v1_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +@preconcurrency +@MainActor +public protocol NSViewRepresentable: View where Body == Never { + /// The type of view to present. + associatedtype NSViewType: NSView + + /// Creates the view object and configures its initial state. + /// + /// You must implement this method and use it to create your view object. + /// Configure the view using your app's current data and contents of the + /// `context` parameter. The system calls this method only once, when it + /// creates your view for the first time. For all subsequent updates, the + /// system calls the ``NSViewRepresentable/updateNSView(_:context:)`` + /// method. + /// + /// - Parameter context: A context structure containing information about + /// the current state of the system. + /// + /// - Returns: Your AppKit view configured with the provided information. + func makeNSView(context: Context) -> NSViewType + + /// Updates the state of the specified view with new information from + /// OpenSwiftUI. + /// + /// When the state of your app changes, OpenSwiftUI updates the portions of your + /// interface affected by those changes. OpenSwiftUI calls this method for any + /// changes affecting the corresponding AppKit view. Use this method to + /// update the configuration of your view to match the new state information + /// provided in the `context` parameter. + /// + /// - Parameters: + /// - nsView: Your custom view object. + /// - context: A context structure containing information about the current + /// state of the system. + func updateNSView(_ nsView: NSViewType, context: Context) + + @_spi(Private) + @available(OpenSwiftUI_v3_0, *) + func _resetNSView(_ nsView: NSViewType, coordinator: Coordinator, destroy: () -> Void) + + + /// Cleans up the presented AppKit view (and coordinator) in anticipation of + /// their removal. + /// + /// Use this method to perform additional clean-up work related to your + /// custom view. For example, you might use this method to remove observers + /// or update other parts of your OpenSwiftUI interface. + /// + /// - Parameters: + /// - nsView: Your custom view object. + /// - coordinator: The custom coordinator you use to communicate changes + /// back to SwiftUI. If you do not use a custom coordinator instance, the + /// system provides a default instance. + static func dismantleNSView(_ nsView: NSViewType, coordinator: Coordinator) + + /// A type to coordinate with the view. + associatedtype Coordinator = Void + + /// Creates the custom instance that you use to communicate changes from + /// your view to other parts of your OpenSwiftUI interface. + /// + /// Implement this method if changes to your view might affect other parts + /// of your app. In your implementation, create a custom Swift instance that + /// can communicate with other parts of your interface. For example, you + /// might provide an instance that binds its variables to OpenSwiftUI + /// properties, causing the two to remain synchronized. If your view doesn't + /// interact with other parts of your app, you don't have to provide a + /// coordinator. + /// + /// SwiftUI calls this method before calling the + /// ``NSViewRepresentable/makeNSView(context:)`` method. The system provides + /// your coordinator instance either directly or as part of a context + /// structure when calling the other methods of your representable instance. + func makeCoordinator() -> Coordinator + + /// Returns the tree of identified views within the platform view. + func _identifiedViewTree(in nsView: NSViewType) -> _IdentifiedViewTree + + /// Given a proposed size, returns the preferred size of the composite view. + /// + /// This method may be called more than once with different proposed sizes + /// during the same layout pass. SwiftUI views choose their own size, so one + /// of the values returned from this function will always be used as the + /// actual size of the composite view. + /// + /// - Parameters: + /// - proposal: The proposed size for the view. + /// - nsView: Your custom view object. + /// - context: A context structure containing information about the + /// current state of the system. + /// + /// - Returns: The composite size of the represented view controller. + /// Returning a value of `nil` indicates that the system should use the + /// default sizing algorithm. + @available(OpenSwiftUI_v4_0, *) + @MainActor + @preconcurrency + func sizeThatFits( + _ proposal: ProposedViewSize, + nsView: NSViewType, + context: Context + ) -> CGSize? + + /// Overrides the default size-that-fits. + func _overrideSizeThatFits( + _ size: inout CGSize, + in proposedSize: _ProposedSize, + nsView: NSViewType + ) + + /// Custom layoutTraits hook. + func _overrideLayoutTraits( + _ layoutTraits: inout _LayoutTraits, + for nsView: NSViewType + ) + + /// Modify inherited view inputs that would be inherited by any contained + /// host views. + /// + /// This specifically allows those inputs to be modified at the bridging + /// point between representable and inner host, which is distinct from the + /// inputs provided to the view representable view itself, as well as from + /// the inputs constructed by a child host (though is used as a set of + /// partial inputs for the latter). + @available(OpenSwiftUI_v2_3, *) + static func _modifyBridgedViewInputs(_ inputs: inout _ViewInputs) + + @available(OpenSwiftUI_v4_0, *) + static var _invalidatesSizeOnConstraintChanges: Bool { get } + + /// Provides options for the specified platform view, which can be used to + /// drive the bridging implementation for the representable. + @available(OpenSwiftUI_v4_0, *) + static func _layoutOptions(_ provider: NSViewType) -> LayoutOptions + + typealias Context = NSViewRepresentableContext + + @available(OpenSwiftUI_v4_0, *) + typealias LayoutOptions = _PlatformViewRepresentableLayoutOptions +} + +// MARK: - NSViewRepresentable + Extension + +@available(OpenSwiftUI_v1_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension NSViewRepresentable where Coordinator == () { + public func makeCoordinator() -> Coordinator { + _openSwiftUIEmptyStub() + } +} + +@available(OpenSwiftUI_v1_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension NSViewRepresentable { + @available(OpenSwiftUI_v3_0, *) + public func _resetNSView( + _ nsView: NSViewType, + coordinator: Coordinator, + destroy: () -> Void + ) { + destroy() + } + + public static func dismantleNSView( + _ nsView: NSViewType, + coordinator: Coordinator + ) { + _openSwiftUIEmptyStub() + } + + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + typealias Adapter = PlatformViewRepresentableAdaptor + precondition(isLinkedOnOrAfter(.v4) ? Metadata(Self.self).isValueType : true, "NSViewRepresentables must be value types: \(Self.self)") + return Adapter._makeView(view: view.unsafeBitCast(to: Adapter.self), inputs: inputs) + } + + nonisolated public static func _makeViewList( + view: _GraphValue, + inputs: _ViewListInputs + ) -> _ViewListOutputs { + .unaryViewList(view: view, inputs: inputs) + } + + public func _identifiedViewTree( + in nsView: NSViewType + ) -> _IdentifiedViewTree { + .empty + } + + @available(OpenSwiftUI_v4_0, *) + public func sizeThatFits( + _ proposal: ProposedViewSize, + nsView: NSViewType, + context: Context + ) -> CGSize? { + nil + } + + public func _overrideSizeThatFits( + _ size: inout CGSize, + in proposedSize: _ProposedSize, + nsView: NSViewType + ) { + _openSwiftUIEmptyStub() + } + + + public func _overrideLayoutTraits( + _ layoutTraits: inout _LayoutTraits, + for nsView: NSViewType + ) { + _openSwiftUIEmptyStub() + } + + @available(OpenSwiftUI_v2_3, *) + public static func _modifyBridgedViewInputs( + _ inputs: inout _ViewInputs + ) { + _openSwiftUIEmptyStub() + } + + @available(OpenSwiftUI_v4_0, *) + public static var _invalidatesSizeOnConstraintChanges: Bool { + true + } + + @available(OpenSwiftUI_v4_0, *) + public static func _layoutOptions( + _ provider: NSViewType + ) -> LayoutOptions { + .init(rawValue: 1) + } + + public var body: Never { + bodyError() + } +} + +// MARK: - PlatformViewRepresentableAdaptor + +private struct PlatformViewRepresentableAdaptor: PlatformViewRepresentable where Base: NSViewRepresentable { + var base: Base + + static var dynamicProperties: DynamicPropertyCache.Fields { + DynamicPropertyCache.fields(of: Base.self) + } + + typealias PlatformViewProvider = Base.NSViewType + + typealias Coordinator = Base.Coordinator + + func makeViewProvider(context: Context) -> PlatformViewProvider { + base.makeNSView(context: .init(context)) + } + + func updateViewProvider(_ provider: PlatformViewProvider, context: Context) { + base.updateNSView(provider, context: .init(context)) + } + + func resetViewProvider(_ provider: PlatformViewProvider, coordinator: Coordinator, destroy: () -> Void) { + base._resetNSView(provider, coordinator: coordinator, destroy: destroy) + } + + static func dismantleViewProvider(_ provider: PlatformViewProvider, coordinator: Coordinator) { + Base.dismantleNSView(provider, coordinator: coordinator) + } + + func makeCoordinator() -> Coordinator { + base.makeCoordinator() + } + + func _identifiedViewTree(in provider: PlatformViewProvider) -> _IdentifiedViewTree { + base._identifiedViewTree(in: provider) + } + + func sizeThatFits(_ proposal: ProposedViewSize, provider: PlatformViewProvider, context: Context) -> CGSize? { + base.sizeThatFits(proposal, nsView: provider, context: .init(context)) + } + + func overrideSizeThatFits(_ size: inout CGSize, in proposedSize: _ProposedSize, platformView: PlatformViewProvider) { + base._overrideSizeThatFits(&size, in: proposedSize, nsView: platformView) + } + + func overrideLayoutTraits(_ traits: inout _LayoutTraits, for provider: PlatformViewProvider) { + base._overrideLayoutTraits(&traits, for: provider) + } + + static func modifyBridgedViewInputs(_ inputs: inout _ViewInputs) { + Base._modifyBridgedViewInputs(&inputs) + } + + static func shouldEagerlyUpdateSafeArea(_ provider: Base.NSViewType) -> Bool { + false + } + + static func layoutOptions(_ provider: PlatformViewProvider) -> LayoutOptions { + Base._layoutOptions(provider) + } +} + +// MARK: - NSViewRepresentableContext + +/// Contextual information about the state of the system that you use to create +/// and update your AppKit view. +/// +/// An ``NSViewRepresentableContext`` structure contains details about the +/// current state of the system. When creating and updating your view, the +/// system creates one of these structures and passes it to the appropriate +/// method of your custom ``NSViewRepresentable`` instance. Use the information +/// in this structure to configure your view. For example, use the provided +/// environment values to configure the appearance of your view. Don't create +/// this structure yourself. +@available(OpenSwiftUI_v1_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +@preconcurrency +@MainActor +public struct NSViewRepresentableContext where View: NSViewRepresentable { + var values: RepresentableContextValues + + /// An instance you use to communicate your AppKit view's behavior and state + /// out to OpenSwiftUI objects. + /// + /// The coordinator is a custom instance you define. When updating your + /// view, communicate changes to OpenSwiftUI by updating the properties of your + /// coordinator, or by calling relevant methods to make those changes. The + /// implementation of those properties and methods are responsible for + /// updating the appropriate SwiftUI values. For example, you might define a + /// property in your coordinator that binds to a OpenSwiftUI value, as shown in + /// the following code example. Changing the property updates the value of + /// the corresponding OpenSwiftUI variable. + /// + /// class Coordinator: NSObject { + /// @Binding var rating: Int + /// init(rating: Binding) { + /// $rating = rating + /// } + /// } + /// + /// To create and configure your custom coordinator, implement the + /// ``NSViewControllerRepresentable/makeCoordinator()`` method of your + /// ``NSViewControllerRepresentable`` object. + public let coordinator: View.Coordinator + + /// The current transaction. + public var transaction: Transaction { + values.transaction + } + + /// Environment data that describes the current state of the system. + /// + /// Use the environment values to configure the state of your view when + /// creating or updating it. + public var environment: EnvironmentValues { + switch values.environmentStorage { + case let .eager(environmentValues): + environmentValues + case let .lazy(attribute, anyRuleContext): + Update.ensure { anyRuleContext[attribute] } + } + } + + init(_ context: PlatformViewRepresentableContext) where R: PlatformViewRepresentable, R.Coordinator == View.Coordinator { + values = context.values + coordinator = context.coordinator + } +} + +@available(OpenSwiftUI_v6_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension NSViewRepresentableContext { + /// Animates changes using the animation in the current transaction. + /// + /// This combines + /// [animate(with:changes:completion:)](https://developer.apple.com/documentation/appkit/nsanimationcontext/4433144-animate) + /// with the current transaction's animation. When you start a OpenSwiftUI + /// animation using ``OpenSwiftUI/withAnimation(_:_:)`` and have a mutated + /// OpenSwiftUI state that causes the representable object to update, use + /// this method to animate changes in the representable object using the + /// same `Animation` timing. + /// + /// struct ContentView: View { + /// @State private var isCollapsed = false + /// var body: some View { + /// ZStack { + /// MyDetailView(isCollapsed: isCollapsed) + /// MyRepresentable(isCollapsed: $isCollapsed) + /// Button("Collapse Content") { + /// withAnimation(.bouncy) { + /// isCollapsed = true + /// } + /// } + /// } + /// } + /// } + /// + /// struct MyRepresentable: NSViewRepresentable { + /// @Binding var isCollapsed: Bool + /// + /// func updateNSView(_ nsView: NSViewType, context: Context) { + /// if isCollapsed && !nsView.isCollapsed { + /// context.animate { + /// nsView.collapseSubview() + /// nsView.layoutSubtreeIfNeeded() + /// } + /// } + /// } + /// } + /// + /// + /// - Parameters: + /// - changes: A closure that changes animatable properties. + /// - completion: A closure to execute after the animation completes. + public func animate(changes: () -> Void, completion: (() -> Void)? = nil) { + if let animation = transaction.animation, + !transaction.disablesAnimations + { + // TODO: OpenSwiftUI + AppKit shims +// NSAnimationContext.animate(animation, changes: changes, +// completion: completion) + } else { + changes() + completion?() + } + } +} +#endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift new file mode 100644 index 000000000..76d27bf6c --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift @@ -0,0 +1,61 @@ +// +// AnyPlatformViewHost.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +import OpenGraphShims + +// MARK: - AnyPlatformViewHost + +protocol AnyPlatformViewHost: AnyObject { + var responder: PlatformViewResponder? { get } +} + +// MARK: - AnyPlatformViewProviderHost + +protocol AnyPlatformViewProviderHost { + associatedtype PlatformViewProvider + + var representedViewProvider: PlatformViewProvider { get } +} + +// MARK: - PlatformLayoutContainer + +protocol PlatformLayoutContainer: AnyObject { + func enqueueLayoutInvalidation() +} + +// MARK: - EmptyPreferenceImporter + +class EmptyPreferenceImporter { + init(graph: ViewGraph) { + _openSwiftUIUnimplementedWarning() + // _openSwiftUIBaseClassAbstractMethod() + } + + func writePreferences( + to outputs: inout _ViewOutputs, + inputs: _ViewInputs + ) { + _openSwiftUIBaseClassAbstractMethod() + } +} + +// MARK: - PlatformViewLayoutInvalidator + +struct PlatformViewLayoutInvalidator { + weak var graphHost: GraphHost? + var layoutComputer: WeakAttribute +} + +// FIXME: Gesture System + +#if canImport(AppKit) +class NSViewResponder {} +#elseif canImport(UIKit) +class UIViewResponder {} +#endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift new file mode 100644 index 000000000..251fdaaf2 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift @@ -0,0 +1,18 @@ +// +// PlatformViewCoordinator.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +import Foundation +import OpenSwiftUICore + +// MARK: - PlatformViewCoordinator + +#if canImport(Darwin) +@objc +#endif +class PlatformViewCoordinator: NSObject {} + +// TODO: weakDispatchUpdate diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift new file mode 100644 index 000000000..0042e2097 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewHost.swift @@ -0,0 +1,297 @@ +// +// PlatformViewHost.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: BAC0C59DB8B5BCCA55C0A700794CB41E (SwiftUI) + +#if canImport(Darwin) +import OpenSwiftUICore +import Foundation +import OpenSwiftUI_SPI + +#if os(iOS) +import UIKit +typealias PlatformConstraintBasedLayoutHostingView = _UIConstraintBasedLayoutHostingView +#elseif os(macOS) +import AppKit +typealias PlatformConstraintBasedLayoutHostingView = _NSConstraintBasedLayoutHostingView +#endif + +// MARK: - PlatformViewHost [WIP] + +final class PlatformViewHost: + PlatformConstraintBasedLayoutHostingView, + AnyPlatformViewHost, + AnyPlatformViewProviderHost +where Content: PlatformViewRepresentable { + var importer: EmptyPreferenceImporter + + var environment: EnvironmentValues + + var viewPhase: _GraphInputs.Phase + + let representedViewProvider: Content.PlatformViewProvider + + weak var host: ViewRendererHost? = nil + + enum ViewControllerParentingMode { + case willMoveToSuperview + case didMoveToWindow + } + + var viewHierarchyMode: ViewControllerParentingMode? + + var focusedValues: FocusedValues + + weak var responder: PlatformViewResponder? + + let safeAreaHelper: PlatformView.SafeAreaHelper = .init() + + #if os(iOS) + var _safeAreaInsets: PlatformEdgeInsets = .init( + top: .greatestFiniteMagnitude, + left: .greatestFiniteMagnitude, + bottom: .greatestFiniteMagnitude, + right: .greatestFiniteMagnitude + ) + #endif + + #if os(macOS) + var recursiveIgnoreHitTest: Bool = false + + var customAcceptsFirstMouse: Bool? + #endif + + var inLayoutSizeThatFits: Bool = false + + var cachedImplementsFittingSize: Bool? + + var layoutInvalidator: PlatformViewLayoutInvalidator? + + var invalidationPending: Bool = false + + var cachedLayoutTraits: _LayoutTraits? + + // FIXME: macOS + init( + _ provider: Content.PlatformViewProvider, + host: ViewRendererHost?, + environment: EnvironmentValues, + viewPhase: _GraphInputs.Phase, + importer: EmptyPreferenceImporter + ) { + self.environment = environment + self.viewPhase = viewPhase + self.representedViewProvider = provider + self.host = host + self.focusedValues = .init() + self.importer = importer + super.init(hostedView: nil) + if Content.isViewController { + viewHierarchyMode = isLinkedOnOrAfter(.v6) ? .willMoveToSuperview : .didMoveToWindow + } + #if os(iOS) + if isLinkedOnOrAfter(.v6) { + layer.allowsGroupOpacity = false + layer.allowsGroupBlending = false + } + #endif + if !Content.isViewController { + hostedView = Content.platformView(for: representedViewProvider) + } + updateEnvironment(environment, viewPhase: viewPhase) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func intrinsicLayoutTraits() -> _LayoutTraits { + #if os(iOS) + func dimension(value: CGFloat, axis: NSLayoutConstraint.Axis) -> _LayoutTraits.Dimension { + if value == PlatformView.noIntrinsicMetric { + return .init(min: .zero, ideal: .zero, max: .infinity) + } else { + let idealValue = max(value, .zero) + let compression = contentCompressionResistancePriority(for: axis) + let hugging = contentHuggingPriority(for: axis) + let minValue = compression >= .defaultHigh ? idealValue : .zero + let maxValue = hugging >= .defaultHigh ? idealValue : .infinity + return .init(min: minValue, ideal: idealValue, max: maxValue) + } + } + let width = dimension(value: intrinsicContentSize.width, axis: .horizontal) + let height = dimension(value: intrinsicContentSize.height, axis: .vertical) + return .init(width: width, height: height) + #elseif os(macOS) + let selector = Selector(("measureMin:max:ideal:stretchingPriority:")) + // TODO + return .init() + #endif + } + + func layoutTraits() -> _LayoutTraits { + guard let cachedLayoutTraits else { + let traits = intrinsicLayoutTraits() + cachedLayoutTraits = traits + return traits + } + return cachedLayoutTraits + } + + func updateEnvironment(_ environment: EnvironmentValues, viewPhase: _GraphInputs.Phase) { + _openSwiftUIUnimplementedWarning() + } + + func updateSafeAreaInsets(_ insets: PlatformEdgeInsets?) { + safeAreaHelper.updateSafeAreaInsets(insets, delegate: self) + } + + override var hostedView: PlatformView? { + get { super.hostedView } + set { super.hostedView = newValue } + } + + #if os(iOS) + override func didAddSubview(_ subview: PlatformView) { + super.didAddSubview(subview) + guard let viewController = representedViewProvider as? PlatformViewController else { + return + } + let view = viewController.view + if hostedView == nil, view != nil { + hostedView = view + } + } + + override func _setHostsLayoutEngine(_ hostsLayoutEngine: Bool) { + guard enableUnifiedLayout() else { + return + } + super._setHostsLayoutEngine(hostsLayoutEngine) + } + #endif + + #if os(iOS) + private func layoutHostedView() { + let enableUnifiedLayout = enableUnifiedLayout() + guard let hostedView else { + return + } + let wantsConstraintBasedLayout = hostedView._wantsConstraintBasedLayout + guard !enableUnifiedLayout || !wantsConstraintBasedLayout else { + return + } + guard bounds.width != .zero, bounds.height != .zero else { + return + } + hostedView.frame = hostedView.frame(forAlignmentRect: bounds) + } + + override func layoutSubviews() { + super.layoutSubviews() + layoutHostedView() + } + #elseif os(macOS) + func updateHostedViewBounds() { + let platformView = Content.platformView(for: representedViewProvider) + platformView.frame = platformView.frame(forAlignmentRect: bounds) + } + + override func layout() { + updateHostedViewBounds() + } + #endif + + #if os(iOS) + override func contentCompressionResistancePriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority { + if Content.isViewController { + return super.contentCompressionResistancePriority(for: axis) + } else { + let platformView = Content.platformView(for: representedViewProvider) + return platformView.contentCompressionResistancePriority(for: axis) + } + } + + override func contentHuggingPriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority { + if Content.isViewController { + return super.contentHuggingPriority(for: axis) + } else { + let platformView = Content.platformView(for: representedViewProvider) + return platformView.contentHuggingPriority(for: axis) + } + } + #elseif os(macOS) + + override func contentCompressionResistancePriority(for orientation: NSLayoutConstraint.Orientation) -> NSLayoutConstraint.Priority { + if Content.isViewController { + return super.contentCompressionResistancePriority(for: orientation) + } else { + let platformView = Content.platformView(for: representedViewProvider) + return platformView.contentCompressionResistancePriority(for: orientation) + } + } + + override func contentHuggingPriority(for orientation: NSLayoutConstraint.Orientation) -> NSLayoutConstraint.Priority { + if Content.isViewController { + return super.contentHuggingPriority(for: orientation) + } else { + let platformView = Content.platformView(for: representedViewProvider) + return platformView.contentHuggingPriority(for: orientation) + } + } + #endif + + #if os(macOS) + override var computedSafeAreaInsets: PlatformEdgeInsets { + safeAreaHelper.resolvedSafeAreaInsets(delegate: self) + } + #endif +} + +extension PlatformViewHost: SafeAreaHelperDelegate { + #if os(macOS) + var _safeAreaInsets: PlatformEdgeInsets { + get { + _openSwiftUIUnimplementedWarning() + return .zero + } + set { + _openSwiftUIUnimplementedWarning() + } + } + #endif + + var defaultSafeAreaInsets: PlatformEdgeInsets { + #if os(iOS) + super.safeAreaInsets + #elseif os(macOS) + super.computedSafeAreaInsets + #endif + } + + var containerView: PlatformView { + self + } + + var shouldEagerlyUpdatesSafeArea: Bool { + Content.shouldEagerlyUpdateSafeArea(representedViewProvider) + } +} +#endif + +func enableUnifiedLayout() -> Bool { + isLinkedOnOrAfter(.maximal) || EnableUnifiedLayoutFeature.isEnabled +} + +// MARK: - EnableUnifiedLayoutFeature + +private struct EnableUnifiedLayoutFeature: UserDefaultKeyedFeature { + static var key: String { + "org.OpenSwiftUIProject.OpenSwiftUI.EnableUnifiedLayout" + } + + static var cachedValue: Bool? +} diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift index 89c65ee9a..5b9c10604 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift @@ -2,35 +2,43 @@ // PlatformViewRepresentable.swift // OpenSwiftUI // -// Audited for iOS 18.0 +// Audited for 6.5.4 // Status: WIP // ID: A513612C07DFA438E70B9FA90719B40D (SwiftUI) -#if canImport(AppKit) -import AppKit -typealias PlatformView = NSView -typealias PlatformViewController = NSViewController -#elseif canImport(UIKit) +#if os(iOS) import UIKit typealias PlatformView = UIView typealias PlatformViewController = UIViewController +typealias PlatformHostingController = UIHostingController +typealias PlatformViewResponder = UIViewResponder +#elseif os(macOS) +import AppKit +typealias PlatformView = NSView +typealias PlatformViewController = NSViewController +typealias PlatformHostingController = NSHostingController +typealias PlatformViewResponder = NSViewResponder #else import Foundation typealias PlatformView = NSObject typealias PlatformViewController = NSObject +typealias PlatformHostingController = NSObject +typealias PlatformViewResponder = NSObject #endif + +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore import OpenGraphShims // MARK: - PlatformViewRepresentable protocol PlatformViewRepresentable: View { - static var dynamicProperties: DynamicPropertyCache.Fields { get } - associatedtype PlatformViewProvider associatedtype Coordinator + static var dynamicProperties: DynamicPropertyCache.Fields { get } + func makeViewProvider(context: Context) -> PlatformViewProvider func updateViewProvider(_ provider: PlatformViewProvider, context: Context) @@ -64,7 +72,7 @@ protocol PlatformViewRepresentable: View { typealias LayoutOptions = _PlatformViewRepresentableLayoutOptions } -// MARK: - PlatformViewRepresentable + Extension [WIP] +// MARK: - PlatformViewRepresentable + Extension extension PlatformViewRepresentable { static var dynamicProperties: DynamicPropertyCache.Fields { @@ -72,8 +80,59 @@ extension PlatformViewRepresentable { } nonisolated static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + #if canImport(Darwin) + guard !inputs.archivedView.isArchived else { + var outputs = _ViewOutputs() + guard inputs.preferences.requiresDisplayList else { + return outputs + } + let identity = DisplayList.Identity() + inputs.pushIdentity(identity) + outputs.displayList = Attribute( + PlatformArchivedDisplayList( + identity: identity, + view: view.value, + position: inputs.animatedPosition(), + size: inputs.animatedSize(), + containerPosition: inputs.containerPosition + ) + ) + return outputs + } + var inputs = inputs + let bridge = PreferenceBridge() + let fields = dynamicProperties + let buffer = _DynamicPropertyBuffer( + fields: fields, + container: view, + inputs: &inputs.base + ) + let child = Attribute( + PlatformViewChild( + view: view.value, + environment: inputs.environment, + transaction: inputs.transaction, + phase: inputs.viewPhase, + position: inputs.position, + size: inputs.size, + transform: inputs.transform, + focusedValues: inputs.base[FocusedValuesInputKey.self], + parentID: inputs.scrapeableParentID, + bridge: bridge, + importer: .init(graph: .current), // FIXME + links: buffer, + coordinator: nil, + platformView: nil + ) + ) + buffer.traceMountedProperties(to: view, fields: fields) // TODO + var outputs = PlatformViewChild.Value.makeDebuggableView(view: .init(child), inputs: inputs) + // TODO + return outputs + #else _openSwiftUIUnimplementedFailure() + #endif } var body: Never { @@ -81,7 +140,7 @@ extension PlatformViewRepresentable { } } -#if canImport(UIKit) || canImport(AppKit) +#if canImport(Darwin) extension PlatformViewRepresentable where PlatformViewProvider: PlatformView { static func platformView(for provider: PlatformViewProvider) -> PlatformView { @@ -99,59 +158,492 @@ extension PlatformViewRepresentable where PlatformViewProvider: PlatformViewCont static var isViewController: Bool { true } } -#endif +// MARK: - PlatformViewChild [WIP] + +// TODO: ScrapeableAttribute +struct PlatformViewChild: StatefulRule { + @Attribute var view: Content + @Attribute var environment: EnvironmentValues + @Attribute var transaction: Transaction + @Attribute var phase: _GraphInputs.Phase + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + @OptionalAttribute var focusedValues: FocusedValues? + let parentID: ScrapeableID + let bridge: PreferenceBridge + let importer: EmptyPreferenceImporter + var links: _DynamicPropertyBuffer + var coordinator: Content.Coordinator? + var platformView: PlatformViewHost? + var resetSeed: UInt32 + let tracker: PropertyList.Tracker + + init( + view: Attribute, + environment: Attribute, + transaction: Attribute, + phase: Attribute<_GraphInputs.Phase>, + position: Attribute, + size: Attribute, + transform: Attribute, + focusedValues: OptionalAttribute, + parentID: ScrapeableID, + bridge: PreferenceBridge, + importer: EmptyPreferenceImporter, + links: _DynamicPropertyBuffer, + coordinator: Content.Coordinator?, + platformView: PlatformViewHost?, + resetSeed: UInt32 = 0 + ) { + self._view = view + self._environment = environment + self._transaction = transaction + self._phase = phase + self._position = position + self._size = size + self._transform = transform + self._focusedValues = focusedValues + self.parentID = parentID + self.bridge = bridge + self.importer = importer + self.links = links + self.coordinator = coordinator + self.platformView = platformView + self.resetSeed = resetSeed + self.tracker = .init() + } + + typealias Value = ViewLeafView + + mutating func updateValue() { + Signpost.platformUpdate.traceInterval( + object: nil, + "PlatformUpdate: (%p) %{public}@ [ %p ]", + [ + AnyAttribute.current!.graph.graphIdentity(), + "\(Content.self)", + platformView.map { UInt(bitPattern: Unmanaged.passUnretained($0).toOpaque()) } ?? 0, + ] + ) { + // TODO + if coordinator == nil { + coordinator = view.makeCoordinator() + } + if platformView == nil { + let host = ViewGraph.viewRendererHost + // TODO: StatefulRule.withObservation + Graph.withoutUpdate { + let representableContext = PlatformViewRepresentableContext( + coordinator: coordinator!, + preferenceBridge: bridge, + transaction: transaction, + environmentStorage: .eager(environment) + ) + representableContext.values.asCurrent { + let provider = view.makeViewProvider(context: representableContext) + let environment = environment.removingTracker() + platformView = PlatformViewHost( + provider, + host: host, + environment: environment, + viewPhase: phase, + importer: importer + ) + } + } + } + value = ViewLeafView( + content: view, + platformView: platformView!, + coordinator: coordinator! + ) + } + } + + private func reset() { + // TODO + } +} + +// MARK: - ViewLeafView + +struct ViewLeafView: PrimitiveView, UnaryView where Content: PlatformViewRepresentable { + let content: Content + var platformView: PlatformViewHost + var coordinator: Content.Coordinator + + init( + content: Content, + platformView: PlatformViewHost, + coordinator: Content.Coordinator + ) { + self.content = content + self.platformView = platformView + self.coordinator = coordinator + } + + var representedViewProvider: Content.PlatformViewProvider { + platformView.representedViewProvider + } + + func layoutTraits() -> _LayoutTraits { + Graph.withoutUpdate { + var traits = platformView.layoutTraits() + content.overrideLayoutTraits(&traits, for: representedViewProvider) + return traits + } + } + + func sizeThatFits( + in proposedSize: _ProposedSize, + environment: Attribute, + context: AnyRuleContext + ) -> CGSize { + var size: CGSize = .zero + Update.syncMain { + let context = PlatformViewRepresentableContext( + coordinator: coordinator, + preferenceBridge: nil, + transaction: .init(), + environmentStorage: .lazy(environment, context) + ) + let result: CGSize + if let fittingSize = content.sizeThatFits( + .init(proposedSize), + provider: representedViewProvider, + context: context + ) { + result = fittingSize + } else { + if enableUnifiedLayout() { + result = unifiedLayoutSize(in: proposedSize) + } else { + let traits = layoutTraits() + result = proposedSize + .fixingUnspecifiedDimensions(at: traits.idealSize) + .clamped(to: traits) + } + } + size = result + } + return size + } + + private func unifiedLayoutSize(in proposedSize: _ProposedSize) -> CGSize { + guard proposedSize.width != nil, proposedSize.height != nil else { + return proposedSize.fixingUnspecifiedDimensions(at: layoutTraits().idealSize) + } + return proposedSize.fixingUnspecifiedDimensions() + } + + nonisolated static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + var outputs = _ViewOutputs() + if inputs.preferences.requiresDisplayList { + let identity = DisplayList.Identity() + inputs.pushIdentity(identity) + outputs.displayList = Attribute( + PlatformViewDisplayList( + identity: identity, + view: view.value, + position: inputs.animatedPosition(), + containerPosition: inputs.containerPosition, + size: inputs.animatedSize(), + transform: inputs.transform, + environment: inputs.environment, + safeAreaInsets: inputs.safeAreaInsets, + contentSeed: .init() + ) + ) + } + if inputs.requestsLayoutComputer { + outputs.layoutComputer = Attribute( + InvalidatableLeafLayoutComputer( + view: view.value, + environment: Attribute( + LeafLayoutEnvironment( + environment: inputs.environment, + tracker: .init() + ) + ), + graphHost: .currentHost + ) + ) + } + return outputs + } +} + +// MARK: - ViewLeafView + PlatformViewFactory + +extension ViewLeafView: PlatformViewFactory { + func makePlatformView() -> AnyObject? { + platformView + } + + func updatePlatformView(_ view: inout AnyObject) { + view = platformView + } + + func renderPlatformView(in ctx: GraphicsContext, size: CGSize, renderer: DisplayList.GraphicsRenderer) { + Update.syncMain { + renderer.renderPlatformView( + platformView, + in: ctx, + size: size, + viewType: Content.self + ) + } + } +} + +// MARK: - PlatformArchivedDisplayList + +struct PlatformArchivedDisplayList: Rule where Content: PlatformViewRepresentable { + let identity: DisplayList.Identity + @Attribute var view: Content + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var containerPosition: CGPoint -// MARK: - RepresentableContextValues + init( + identity: DisplayList.Identity, + view: Attribute, + position: Attribute, + size: Attribute, + containerPosition: Attribute + ) { + self.identity = identity + self._view = view + self._position = position + self._size = size + self._containerPosition = containerPosition + } + + var value: DisplayList { + let version = DisplayList.Version(forUpdate: ()) + let contentSeed = DisplayList.Seed(version) + let content = DisplayList.Content( + .platformView(Factory()), + seed: contentSeed + ) + let frame = CGRect( + origin: CGPoint(position - containerPosition), + size: size.value + ) + var item = DisplayList.Item( + .content(content), + frame: frame, + identity: identity, + version: version + ) + item.canonicalize() + return DisplayList(item) + } + + struct Factory: PlatformViewFactory { + var viewType: any Any.Type { + Content.self + } + + func makePlatformView() -> AnyObject? { + preconditionFailure("") + } + + func updatePlatformView(_ view: inout AnyObject) { + preconditionFailure("") + } + } +} -struct RepresentableContextValues { - static var current: RepresentableContextValues? +// MARK: - InvalidatableLeafLayoutComputer - var preferenceBridge: PreferenceBridge? +private struct InvalidatableLeafLayoutComputer: StatefulRule, CustomStringConvertible where Content: PlatformViewRepresentable { + @Attribute var view: ViewLeafView + @Attribute var environment: EnvironmentValues + weak var graphHost: GraphHost? - var transaction: Transaction + typealias Value = LayoutComputer - var environmentStorage: EnvironmentStorage + mutating func updateValue() { + if view.platformView.layoutInvalidator == nil { + view.platformView.layoutInvalidator = PlatformViewLayoutInvalidator( + graphHost: graphHost, + layoutComputer: WeakAttribute(attribute) + ) + } + let engine = PlatformViewLayoutEngine( + view: view, + environment: $environment, + context: AnyRuleContext(context) + ) + update(to: engine) + } - enum EnvironmentStorage { - case eager(EnvironmentValues) - case lazy(Attribute, AnyRuleContext) + var description: String { + "InvalidatableLeafLayoutComputer" } +} + +// MARK: - LeafLayoutEnvironment + +private struct LeafLayoutEnvironment: StatefulRule { + @Attribute var environment: EnvironmentValues + let tracker: PropertyList.Tracker + + typealias Value = EnvironmentValues - func asCurrent(do: () -> V) -> V { - let old = Self.current - Self.current = self - defer { Self.current = old } - return `do`() + func updateValue() { + let (env, envChanged) = $environment.changedValue() + let shouldReset: Bool + if !hasValue { + shouldReset = true + } else if envChanged, tracker.hasDifferentUsedValues(env.plist) { + shouldReset = true + } else { + shouldReset = false + } + if shouldReset { + tracker.reset() + value = EnvironmentValues( + environment.plist, + tracker: tracker + ) + } } } -// MARK: - PlatformViewRepresentableContext +// MARK: - PlatformViewDisplayList -struct PlatformViewRepresentableContext { - var values: RepresentableContextValues - let coordinator: Representable.Coordinator +private struct PlatformViewDisplayList: StatefulRule where Content: PlatformViewRepresentable { + let identity: DisplayList.Identity + @Attribute var view: ViewLeafView + @Attribute var position: ViewOrigin + @Attribute var containerPosition: ViewOrigin + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + @Attribute var environment: EnvironmentValues + @OptionalAttribute var safeAreaInsets: SafeAreaInsets? + var contentSeed: DisplayList.Seed init( - coordinator: Representable.Coordinator, - preferenceBridge: PreferenceBridge?, - transaction: Transaction, - environmentStorage: RepresentableContextValues.EnvironmentStorage + identity: DisplayList.Identity, + view: Attribute>, + position: Attribute, + containerPosition: Attribute, + size: Attribute, + transform: Attribute, + environment: Attribute, + safeAreaInsets: OptionalAttribute, + contentSeed: DisplayList.Seed ) { - self.values = .init(preferenceBridge: preferenceBridge, transaction: transaction, environmentStorage: environmentStorage) - self.coordinator = coordinator + self.identity = identity + self._view = view + self._position = position + self._containerPosition = containerPosition + self._size = size + self._transform = transform + self._environment = environment + self._safeAreaInsets = safeAreaInsets + self.contentSeed = contentSeed } - @inlinable - var environment: EnvironmentValues { - switch values.environmentStorage { - case let .eager(environmentValues): - environmentValues - case let .lazy(attribute, anyRuleContext): - Update.ensure { anyRuleContext[attribute] } + typealias Value = DisplayList + + mutating func updateValue() { + let version = DisplayList.Version(forUpdate: ()) + let (view, viewChanged) = $view.changedValue() + if viewChanged { + contentSeed = .init(version) + } + var frame = CGRect( + origin: CGPoint(position - containerPosition), + size: size.value + ) + let layoutOption = Content.layoutOptions(view.representedViewProvider) + if layoutOption.contains(.propagatesSafeArea) { + let placementContext = _PositionAwarePlacementContext( + context: AnyRuleContext(context), + size: _size, + environment: _environment, + transform: _transform, + position: _position, + safeAreaInsets: _safeAreaInsets + ) + let edgeInsets = placementContext.safeAreaInsets(matching: .all) + let isRTL = environment.layoutDirection == .rightToLeft + let platformEdgeInsets = PlatformEdgeInsets( + top: edgeInsets.top, + left: isRTL ? edgeInsets.trailing : edgeInsets.leading, + bottom: edgeInsets.bottom, + right: isRTL ? edgeInsets.leading : edgeInsets.trailing + ) + frame.origin -= CGSize(width: platformEdgeInsets.left, height: platformEdgeInsets.top) + frame.size.width += edgeInsets.horizontal + frame.size.height += edgeInsets.vertical + view.platformView.updateSafeAreaInsets(platformEdgeInsets) } + var item = DisplayList.Item( + .content(.init( + .platformView(view), + seed: contentSeed + )), + frame: frame, + identity: identity, + version: version + ) + item.canonicalize() + value = DisplayList(item) } } -// MARK: - PlatformViewCoordinator +// MARK: - PlatformViewLayoutEngine + +private struct PlatformViewLayoutEngine: LayoutEngine where Content: PlatformViewRepresentable { + var cache: ViewSizeCache + var view: ViewLeafView + var environment: Attribute + var context: AnyRuleContext -class PlatformViewCoordinator: NSObject {} + init( + cache: ViewSizeCache = .init(), + view: ViewLeafView, + environment: Attribute, + context: AnyRuleContext + ) { + self.cache = cache + self.view = view + self.environment = environment + self.context = context + } + + mutating func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize { + cache.get(proposedSize) { + view.sizeThatFits( + in: proposedSize, + environment: environment, + context: context + ) + } + } + + func explicitAlignment(_ k: AlignmentKey, at viewSize: ViewSize) -> CGFloat? { + if k == VerticalAlignment.firstTextBaseline.key { + let baseline = view.platformView._baselineOffsets(at: viewSize.value) + let firstTextBaseline = baseline.firstTextBaseline + return firstTextBaseline.isNaN ? .zero : firstTextBaseline + } else if k == VerticalAlignment.lastTextBaseline.key { + let baseline = view.platformView._baselineOffsets(at: viewSize.value) + let lastTextBaseline = baseline.lastTextBaseline + let height = viewSize.height + return lastTextBaseline.isNaN ? height : height - lastTextBaseline + } else { + return nil + } + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableContext.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableContext.swift new file mode 100644 index 000000000..e42438926 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentableContext.swift @@ -0,0 +1,63 @@ +// +// PlatformViewRepresentableContext.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +import OpenGraphShims + +// MARK: - RepresentableContextValues + +struct RepresentableContextValues { + static var current: RepresentableContextValues? + + var preferenceBridge: PreferenceBridge? + + var transaction: Transaction + + var environmentStorage: EnvironmentStorage + + enum EnvironmentStorage { + case eager(EnvironmentValues) + case lazy(Attribute, AnyRuleContext) + } + + func asCurrent(do action: () -> V) -> V { + let old = Self.current + Self.current = self + defer { Self.current = old } + return action() + } +} + +// MARK: - PlatformViewRepresentableContext + +struct PlatformViewRepresentableContext { + var values: RepresentableContextValues + let coordinator: Content.Coordinator + + init( + coordinator: Content.Coordinator, + preferenceBridge: PreferenceBridge?, + transaction: Transaction, + environmentStorage: RepresentableContextValues.EnvironmentStorage + ) { + self.values = .init( + preferenceBridge: preferenceBridge, + transaction: transaction, + environmentStorage: environmentStorage + ) + self.coordinator = coordinator + } + + @inlinable + var environment: EnvironmentValues { + switch values.environmentStorage { + case let .eager(environmentValues): + environmentValues + case let .lazy(attribute, anyRuleContext): + Update.ensure { anyRuleContext[attribute] } + } + } +} diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/SafeAreaHelper.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/SafeAreaHelper.swift new file mode 100644 index 000000000..e1c09ade7 --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/SafeAreaHelper.swift @@ -0,0 +1,64 @@ +// +// SafeAreaHelper.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP (Unimplemented) +// ID: 36F4CE8257AE99191765DF6F47D9C4C0 (SwiftUI?) + +#if canImport(Darwin) + +protocol SafeAreaHelperDelegate: AnyObject { + var _safeAreaInsets: PlatformEdgeInsets { get set } + var defaultSafeAreaInsets: PlatformEdgeInsets { get } + var containerView: PlatformView { get } + var shouldEagerlyUpdatesSafeArea: Bool { get } +} + +#if os(iOS) +import UIKit +typealias PlatformEdgeInsets = UIEdgeInsets +#elseif os(macOS) +import AppKit +typealias PlatformEdgeInsets = NSEdgeInsets +extension NSEdgeInsets { + static var zero: NSEdgeInsets { + NSEdgeInsetsZero + } +} +#endif + +extension PlatformView { + final class SafeAreaHelper { + private var pendingSafeAreaInsets: PlatformEdgeInsets? + private var lastParentSafeAreaInsets: PlatformEdgeInsets? + + func updateSafeAreaInsets( + _ insets: PlatformEdgeInsets?, + delegate: Delegate + ) where Delegate: SafeAreaHelperDelegate { + _openSwiftUIUnimplementedWarning() + } + + func prepareForSafeAreaPropagation( + delegate: Delegate + ) where Delegate: SafeAreaHelperDelegate { + _openSwiftUIUnimplementedWarning() + } + + func resolvedSafeAreaInsets( + delegate: Delegate + ) -> PlatformEdgeInsets where Delegate: SafeAreaHelperDelegate { + _openSwiftUIUnimplementedWarning() + return .zero + } + + private func adjustSafeAreaIfNeeded( + delegate: Delegate + ) where Delegate: SafeAreaHelperDelegate { + _openSwiftUIUnimplementedWarning() + } + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewControllerRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewControllerRepresentable.swift index 011495c6d..83bca8db8 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewControllerRepresentable.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewControllerRepresentable.swift @@ -48,6 +48,7 @@ import OpenGraphShims /// properties. Don't directly set these layout-related properties on the view /// managed by a `UIViewControllerRepresentable` instance from your own /// code because that conflicts with OpenSwiftUI and results in undefined behavior. +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) @available(watchOS, unavailable) @MainActor @@ -89,6 +90,7 @@ public protocol UIViewControllerRepresentable: View where Body == Never { func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) @_spi(Private) + @available(OpenSwiftUI_v3_0, *) func _resetUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator, destroy: () -> Void) /// Cleans up the presented view controller (and coordinator) in @@ -142,51 +144,92 @@ public protocol UIViewControllerRepresentable: View where Body == Never { /// - Returns: The composite size of the represented view controller. /// Returning a value of `nil` indicates that the system should use the /// default sizing algorithm. - func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIViewControllerType, context: Context) -> CGSize? + @available(OpenSwiftUI_v4_0, *) + func sizeThatFits( + _ proposal: ProposedViewSize, + uiViewController: UIViewControllerType, + context: Context + ) -> CGSize? /// Returns the tree of identified views within the platform view. func _identifiedViewTree(in uiViewController: UIViewControllerType) -> _IdentifiedViewTree /// Provides options for the specified platform view, which can be used to /// drive the bridging implementation for the representable. + @available(OpenSwiftUI_v5_0, *) static func _layoutOptions(_ provider: UIViewControllerType) -> LayoutOptions typealias Context = UIViewControllerRepresentableContext + @available(OpenSwiftUI_v5_0, *) typealias LayoutOptions = _PlatformViewRepresentableLayoutOptions } // MARK: - UIViewControllerRepresentable + Extension +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) extension UIViewControllerRepresentable where Coordinator == () { public func makeCoordinator() -> Coordinator { - return + _openSwiftUIEmptyStub() } } +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) extension UIViewControllerRepresentable { - public func _resetUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator, destroy: () -> Void) { + @available(OpenSwiftUI_v3_0, *) + public func _resetUIViewController( + _ uiViewController: UIViewControllerType, + coordinator: Coordinator, + destroy: () -> Void + ) { destroy() } - public func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIViewControllerType, context: Context) -> CGSize? { nil } + public func sizeThatFits( + _ proposal: ProposedViewSize, + uiViewController: UIViewControllerType, + context: Context + ) -> CGSize? { + nil + } - public static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator) {} + public static func dismantleUIViewController( + _ uiViewController: UIViewControllerType, + coordinator: Coordinator + ) { + _openSwiftUIEmptyStub() + } - nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { _openSwiftUIUnimplementedFailure() } - nonisolated public static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { + nonisolated public static func _makeViewList( + view: _GraphValue, + inputs: _ViewListInputs + ) -> _ViewListOutputs { .unaryViewList(view: view, inputs: inputs) } - public func _identifiedViewTree(in uiViewController: UIViewControllerType) -> _IdentifiedViewTree { .empty } + public func _identifiedViewTree( + in uiViewController: UIViewControllerType + ) -> _IdentifiedViewTree { + .empty + } - // FIXME - public static func _layoutOptions(_ provider: UIViewControllerType) -> LayoutOptions { .init(rawValue: 1) } + @available(OpenSwiftUI_v5_0, *) + public static func _layoutOptions( + _ provider: UIViewControllerType + ) -> LayoutOptions { + .init( + rawValue: 1 + ) + } /// Declares the content and behavior of this view. public var body: Never { @@ -223,6 +266,7 @@ private struct UnsupportedDisplayList: Rule { /// controller. For example, use the provided environment values to configure /// the appearance of your view controller and views. Don't create this /// structure yourself. +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) @available(watchOS, unavailable) @MainActor @@ -289,6 +333,7 @@ public struct UIViewControllerRepresentableContext where Represen /// - Parameters: /// - changes: A closure that changes animatable properties. /// - completion: A closure to execute after the animation completes. + @available(OpenSwiftUI_v6_0, *) public func animate(changes: () -> Void, completion: (() -> Void)? = nil) { guard let animation = transaction.animation, !transaction.disablesAnimations else { changes() diff --git a/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewRepresentable.swift index 610ea6518..8f3d7bfd8 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewRepresentable.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/UIKit/UIViewRepresentable.swift @@ -2,7 +2,7 @@ // UIViewRepresentable.swift // OpenSwiftUI // -// Audited for iOS 18.0 +// Audited for 6.5.4 // Status: WIP // ID: 19642D833A8FE469B137699ED1426762 (SwiftUI) @@ -57,6 +57,7 @@ import OpenGraphShims /// properties. Don't directly set these layout-related properties on the view /// managed by a `UIViewRepresentable` instance from your own /// code because that conflicts with OpenSwiftUI and results in undefined behavior. +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) @available(watchOS, unavailable) @MainActor @@ -96,6 +97,7 @@ public protocol UIViewRepresentable: View where Body == Never { func updateUIView(_ uiView: UIViewType, context: Context) @_spi(Private) + @available(OpenSwiftUI_v3_0, *) func _resetUIView(_ uiView: UIViewType, coordinator: Coordinator, destroy: () -> Void) /// Cleans up the presented UIKit view (and coordinator) in anticipation of @@ -151,10 +153,19 @@ public protocol UIViewRepresentable: View where Body == Never { /// - Returns: The composite size of the represented view controller. /// Returning a value of `nil` indicates that the system should use the /// default sizing algorithm. - func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? + @available(OpenSwiftUI_v4_0, *) + func sizeThatFits( + _ proposal: ProposedViewSize, + uiView: UIViewType, + context: Context + ) -> CGSize? /// Overrides the default size-that-fits. - func _overrideSizeThatFits(_ size: inout CGSize, in proposedSize: _ProposedSize, uiView: UIViewType) + func _overrideSizeThatFits( + _ size: inout CGSize, + in proposedSize: _ProposedSize, + uiView: UIViewType + ) /// Custom layoutTraits hook. func _overrideLayoutTraits(_ layoutTraits: inout _LayoutTraits, for uiView: UIViewType) @@ -167,55 +178,108 @@ public protocol UIViewRepresentable: View where Body == Never { /// inputs provided to the view representable view itself, as well as from /// the inputs constructed by a child host (though is used as a set of /// partial inputs for the latter). + @available(OpenSwiftUI_v3_0, *) static func _modifyBridgedViewInputs(_ inputs: inout _ViewInputs) /// Provides options for the specified platform view, which can be used to /// drive the bridging implementation for the representable. + @available(OpenSwiftUI_v5_0, *) static func _layoutOptions(_ provider: UIViewType) -> LayoutOptions typealias Context = UIViewRepresentableContext + @available(OpenSwiftUI_v5_0, *) typealias LayoutOptions = _PlatformViewRepresentableLayoutOptions } // MARK: - UIViewRepresentable + Extension +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) extension UIViewRepresentable where Coordinator == () { public func makeCoordinator() -> Coordinator { - return + _openSwiftUIEmptyStub() } } +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) extension UIViewRepresentable { - public func _resetUIView(_ uiView: UIViewType, coordinator: Coordinator, destroy: () -> Void) { + @available(OpenSwiftUI_v3_0, *) + public func _resetUIView( + _ uiView: UIViewType, + coordinator: Coordinator, + destroy: () -> Void + ) { destroy() } - public static func dismantleUIView(_ uiView: UIViewType, coordinator: Coordinator) {} + public static func dismantleUIView( + _ uiView: UIViewType, + coordinator: Coordinator + ) { + _openSwiftUIEmptyStub() + } - nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + typealias Adapter = PlatformViewRepresentableAdaptor precondition(isLinkedOnOrAfter(.v4) ? Metadata(Self.self).isValueType : true, "UIViewRepresentables must be value types: \(Self.self)") - return PlatformViewRepresentableAdaptor._makeView(view: view.unsafeCast(), inputs: inputs) + return Adapter._makeView(view: view.unsafeBitCast(to: Adapter.self), inputs: inputs) } - nonisolated public static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { + nonisolated public static func _makeViewList( + view: _GraphValue, + inputs: _ViewListInputs + ) -> _ViewListOutputs { .unaryViewList(view: view, inputs: inputs) } - public func _identifiedViewTree(in uiView: UIViewType) -> _IdentifiedViewTree { .empty } + public func _identifiedViewTree( + in uiView: UIViewType + ) -> _IdentifiedViewTree { + .empty + } - public func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? { nil } + @available(OpenSwiftUI_v4_0, *) + public func sizeThatFits( + _ proposal: ProposedViewSize, + uiView: UIViewType, + context: Context + ) -> CGSize? { + nil + } - public func _overrideSizeThatFits(_ size: inout CGSize, in proposedSize: _ProposedSize, uiView: UIViewType) {} + public func _overrideSizeThatFits( + _ size: inout CGSize, + in proposedSize: _ProposedSize, + uiView: UIViewType + ) { + _openSwiftUIEmptyStub() + } - public func _overrideLayoutTraits(_ layoutTraits: inout _LayoutTraits, for uiView: UIViewType) {} + public func _overrideLayoutTraits( + _ layoutTraits: inout _LayoutTraits, + for uiView: UIViewType + ) { + _openSwiftUIEmptyStub() + } - public static func _modifyBridgedViewInputs(_ inputs: inout _ViewInputs) {} + @available(OpenSwiftUI_v3_0, *) + public static func _modifyBridgedViewInputs( + _ inputs: inout _ViewInputs + ) { + _openSwiftUIEmptyStub() + } - // FIXME - public static func _layoutOptions(_ provider: UIViewType) -> LayoutOptions { .init(rawValue: 1) } + @available(OpenSwiftUI_v5_0, *) + public static func _layoutOptions( + _ provider: UIViewType + ) -> LayoutOptions { + .init(rawValue: 1) + } /// Declares the content and behavior of this view. public var body: Never { @@ -236,7 +300,7 @@ private struct PlatformViewRepresentableAdaptor: PlatformViewRepresentable typealias Coordinator = Base.Coordinator - func makeViewProvider(context: Context) -> Base.UIViewType { + func makeViewProvider(context: Context) -> PlatformViewProvider { base.makeUIView(context: .init(context)) } @@ -297,6 +361,7 @@ private struct PlatformViewRepresentableAdaptor: PlatformViewRepresentable /// in this structure to configure your view. For example, use the provided /// environment values to configure the appearance of your view. Don't create /// this structure yourself. +@available(OpenSwiftUI_v1_0, *) @available(macOS, unavailable) @available(watchOS, unavailable) @MainActor @@ -371,6 +436,9 @@ public struct UIViewRepresentableContext where Representable: UIV /// - Parameters: /// - changes: A closure that changes animatable properties. /// - completion: A closure to execute after the animation completes. + @available(OpenSwiftUI_v6_0, *) + @available(macOS, unavailable) + @available(watchOS, unavailable) public func animate(changes: () -> Void, completion: (() -> Void)? = nil) { guard let animation = transaction.animation, !transaction.disablesAnimations else { changes() diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift index aa6938bb8..47671f92b 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift @@ -493,4 +493,16 @@ extension GraphicsContext { package func draw(_ path: Path, with shading: GraphicsContext.ResolvedShading, style: PathDrawingStyle) { _openSwiftUIUnimplementedFailure() } + + // FIXME + #if canImport(CoreGraphics) + static func renderingTo( + cgContext: CGContext, + environment: EnvironmentValues, + deviceScale: CGFloat?, + content: (inout GraphicsContext) -> () + ) { + _openSwiftUIUnimplementedFailure() + } + #endif } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index 68589eb66..bc00cf97b 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -31,6 +31,7 @@ package struct _DisplayList_Identity: Hashable, Codable, CustomStringConvertible } package static let none = _DisplayList_Identity(value: 0) + package var description: String { "#\(value)" } } @@ -182,7 +183,7 @@ extension DisplayList { } } -// package typealias ViewFactory = _DisplayList_ViewFactory + package typealias ViewFactory = _DisplayList_ViewFactory package enum Effect { case identity @@ -448,6 +449,14 @@ extension PreferencesInputs { } } +extension _ViewOutputs { + @inline(__always) + package var displayList: Attribute? { + get { preferences.displayList } + set { preferences.displayList = newValue } + } +} + extension PreferencesOutputs { @inline(__always) package var displayList: Attribute? { @@ -491,16 +500,11 @@ extension GraphicsContext { } } -package protocol PlatformGroupFactory {} package protocol _DisplayList_AnyEffectAnimation {} public struct ContentTransition { package struct State {} } -package protocol _DisplayList_ViewFactory {} - -package protocol PlatformViewFactory {} -package protocol PlatformLayerFactory {} package struct GraphicsImage {} package struct ResolvedShadowStyle {} diff --git a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift index bcb2fe52d..45bdf8664 100644 --- a/Sources/OpenSwiftUICore/Render/RendererLeafView.swift +++ b/Sources/OpenSwiftUICore/Render/RendererLeafView.swift @@ -122,10 +122,10 @@ private struct LeafDisplayList: StatefulRule, CustomStringConvertible where V } mutating func updateValue() { - let (view, changed) = $view.changedValue() + let (view, viewChanged) = $view.changedValue() let content = view.content() let version = DisplayList.Version(forUpdate: ()) - if changed { + if viewChanged { contentSeed = .init(version) } var item = DisplayList.Item( diff --git a/Sources/OpenSwiftUICore/Render/ViewFactory.swift b/Sources/OpenSwiftUICore/Render/ViewFactory.swift new file mode 100644 index 000000000..11c8eb534 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/ViewFactory.swift @@ -0,0 +1,326 @@ +// +// ViewFactory.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// ID: 7A45621CE16223183E03CAC88E8C5E60 (SwiftUICore?) + +package import Foundation +#if canImport(QuartzCore) +package import QuartzCore +#endif + +// MARK: - AnyViewFactory + +package protocol AnyViewFactory { + var viewType: any Any.Type { get } + + func encoding() -> (id: String, data: any Codable)? +} + +extension AnyViewFactory { + package func encoding() -> (id: String, data: any Codable)? { + nil + } +} + +extension AnyViewFactory where Self: View { + package var viewType: any Any.Type { + Self.self + } +} + +// MARK: - PlatformLayerFactory [TODO] + +package protocol PlatformLayerFactory: AnyViewFactory { + #if canImport(QuartzCore) + var platformLayerType: CALayer.Type { get } + + func updatePlatformLayer(_ layer: CALayer) + #endif + + func renderPlatformLayer( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) +} + +extension PlatformLayerFactory { + package func renderPlatformLayer( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + // TODO: render.platformViewMode + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - PlatformViewFactory [TODO] + +package protocol PlatformViewFactory: AnyViewFactory { + func makePlatformView() -> AnyObject? + + func updatePlatformView(_ view: inout AnyObject) + + func renderPlatformView( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) + + var features: DisplayList.Features { get } +} + +extension PlatformViewFactory { + package func renderPlatformView( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + // TODO: render.platformViewMode + _openSwiftUIUnimplementedFailure() + } + + package var features: DisplayList.Features { + [.required] + } +} + +extension RendererLeafView where Self: PlatformViewFactory { + package func content() -> DisplayList.Content.Value { + .platformView(self) + } +} + +// MARK: - PlatformGroupFactory [TODO] + +package protocol PlatformGroupFactory: AnyViewFactory { + func makePlatformGroup() -> AnyObject? + + func needsUpdateFor(newValue: any PlatformGroupFactory) -> Bool + + func updatePlatformGroup(_ view: inout AnyObject) + + func platformGroupContainer(_ view: AnyObject) -> AnyObject + + func renderPlatformGroup( + _ list: DisplayList, + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) + + var features: DisplayList.Features { get } +} + +extension PlatformGroupFactory { + package func renderPlatformGroup( + _ list: DisplayList, + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + // TODO: RB + _openSwiftUIUnimplementedFailure() + } + + package var features: DisplayList.Features { + [.required] + } +} + +// MARK: - DisplayList.ViewFactory + +package protocol _DisplayList_ViewFactory: AnyViewFactory { + func makeView() -> AnyView + + var identity: DisplayList.Identity { get } +} + +extension DisplayList.ViewFactory { + package var identity: DisplayList.Identity { + .none + } +} + +extension RendererLeafView where Self: _DisplayList_ViewFactory { + package func content() -> DisplayList.Content.Value { + .view(self) + } +} + +// MARK: - ViewDecoders + +package struct ViewDecoders { + package typealias DecodableViewFactory = Decodable & AnyViewFactory + + @AtomicBox + private static var shared: ViewDecoders = .init() + + package static func registerDecodableFactoryType( + _ factoryType: T.Type, + forType type: U.Type + ) where T: Decodable, T: AnyViewFactory { + registerDecodableFactoryType( + factoryType, + forID: _typeName(type, qualified: true) + ) + } + + package static func registerDecodableFactoryType( + _ factoryType: T.Type, + forID id: String + ) where T: Decodable, T: AnyViewFactory { + shared.decodableFactoryTypes[id] = factoryType + _openSwiftUIUnimplementedFailure() + } + + package static func registerStandard(_ body: () -> Void) { + body() + } + + private var decodableFactoryTypes: [String: any DecodableViewFactory.Type] = [:] + + private var hasRegisteredStandardDecoders = false +} + +// MARK: - EmptyViewFactory [TODO] + +package struct EmptyViewFactory: AnyViewFactory { + package var viewType: any Any.Type { + EmptyView.self + } + + package init() { + _openSwiftUIEmptyStub() + } +} + +#if canImport(QuartzCore) +extension EmptyViewFactory: PlatformLayerFactory { + private class MissingLayer: CALayer { + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override init(layer: Any) { + super.init(layer: layer) + } + + override init() { + super.init() + } + + override func draw(in ctx: CGContext) { + GraphicsContext.renderingTo( + cgContext: ctx, + environment: .init(), + deviceScale: .zero + ) { _ in + _openSwiftUIUnimplementedFailure() + } + } + + override var needsDisplayOnBoundsChange: Bool { + get { true } + set {} + } + } + + package var platformLayerType: CALayer.Type { + MissingLayer.self + } + + package func updatePlatformLayer(_ view: CALayer) { + _openSwiftUIEmptyStub() + } + + package func renderPlatformLayer( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + _openSwiftUIUnimplementedFailure() + } +} +#endif + +extension EmptyViewFactory: PlatformViewFactory { + package func makePlatformView() -> AnyObject? { + nil + } + + package func updatePlatformView(_ view: inout AnyObject) { + _openSwiftUIEmptyStub() + } + + package var features: DisplayList.Features { + [.required] + } + + package func renderPlatformView( + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + _openSwiftUIUnimplementedFailure() + } +} + +extension EmptyViewFactory: PlatformGroupFactory { + package func makePlatformGroup() -> AnyObject? { + nil + } + + package func needsUpdateFor(newValue: any PlatformGroupFactory) -> Bool { + false + } + + package func updatePlatformGroup(_ view: inout AnyObject) { + _openSwiftUIEmptyStub() + } + + package func platformGroupContainer(_ view: AnyObject) -> AnyObject { + _openSwiftUIUnimplementedFailure() + } + + package func renderPlatformGroup( + _ list: DisplayList, + in ctx: GraphicsContext, + size: CGSize, + renderer: DisplayList.GraphicsRenderer + ) { + _openSwiftUIUnimplementedFailure() + } +} + +extension EmptyViewFactory: DisplayList.ViewFactory { + package func makeView() -> AnyView { + AnyView(EmptyView()) + } +} + +// MARK: - CodableViewFactory + +struct CodableViewFactory: ProtobufMessage { + var factory: any AnyViewFactory + + private enum Error: Swift.Error { + case missingView(String) + case invalidView + } + + init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() + } + + func encode(to encoder: inout ProtobufEncoder) throws { + guard let encoding = factory.encoding() else { + // TODO: encoder.archiveHost + _openSwiftUIUnimplementedFailure() + } + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Tracing/Tracing.swift b/Sources/OpenSwiftUICore/Tracing/Tracing.swift index e4ae410d1..ece0554ab 100644 --- a/Sources/OpenSwiftUICore/Tracing/Tracing.swift +++ b/Sources/OpenSwiftUICore/Tracing/Tracing.swift @@ -87,7 +87,7 @@ package func traceRuleBody(_ v: any Any.Type, body: () -> Body) -> Body { [ current.rawValue, 1, - current.graph.counter(for: ._4) // FIXME: UInt + current.graph.graphIdentity() ] ) #endif @@ -102,3 +102,16 @@ package func traceRuleBody(_ v: any Any.Type, body: () -> Body) -> Body { closure: body ) } + +extension Graph { + package func graphIdentity() -> UInt { + // FIXME: remove numericCast + numericCast(counter(for: ._4)) + } +} + +extension ViewGraph { + package var graphIdentity: UInt { + graph.graphIdentity() + } +} diff --git a/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift b/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift new file mode 100644 index 000000000..a78d93923 --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift @@ -0,0 +1,31 @@ +// +// ScrapeableID.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP + +import OpenGraphShims + +package struct ScrapeableID: Hashable { + + @inlinable + package init() { + value = numericCast(makeUniqueID()) + } + + package static let none = ScrapeableID(value: 0) + + let value: UInt32 + + private init(value: UInt32) { + self.value = value + } +} + +// FIXME +extension _ViewInputs { + package var scrapeableParentID: ScrapeableID { + .none + } +} diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 54bbea614..dc54098c0 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -61,7 +61,7 @@ extension ViewRendererHost { object: self, "", [ - viewGraph.graph.counter(for: ._4), // FIXME: UInt + viewGraph.graph.graphIdentity(), // FIXME: UInt UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque()), ] ) @@ -76,7 +76,7 @@ extension ViewRendererHost { object: self, "", [ - viewGraph.graph.counter(for: ._4), // FIXME: UInt + viewGraph.graph.graphIdentity(), // FIXME: UInt UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque()), ] ) diff --git a/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.h b/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.h index d6ab2a45f..75c242477 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.h @@ -28,6 +28,6 @@ typedef OPENSWIFTUI_ENUM(NSInteger, NSViewVibrantBlendingStyle) { OPENSWIFTUI_ASSUME_NONNULL_END -#endif /* AppKit.h */ +#endif /* __has_include() */ #endif /* AppKit_Private_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.m b/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.m index df9ed293d..280ddf367 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.m +++ b/Sources/OpenSwiftUI_SPI/Shims/AppKit/AppKit_Private.m @@ -28,5 +28,5 @@ - (void)failedTest_openswiftui_safe_wrapper:(NSString *)name withFailure:(NSErro } @end -#endif /* AppKit.h */ +#endif /* __has_include() */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/AppKit/NSView_NSSafeAreas.h b/Sources/OpenSwiftUI_SPI/Shims/AppKit/NSView_NSSafeAreas.h new file mode 100644 index 000000000..d90a59a78 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/AppKit/NSView_NSSafeAreas.h @@ -0,0 +1,20 @@ +// +// NSView_NSSafeAreas.h +// OpenSwiftUI_SPI + +#ifndef NSView_NSSafeAreas_h +#define NSView_NSSafeAreas_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#include + +@interface NSView (NSSafeAreas) +@property (nonatomic, assign, readonly) NSEdgeInsets computedSafeAreaInsets; +@end + +#endif /* __has_include() */ + +#endif /* NSView_NSSafeAreas_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/AppKit/_NSConstraintBasedLayoutHostingView.h b/Sources/OpenSwiftUI_SPI/Shims/AppKit/_NSConstraintBasedLayoutHostingView.h new file mode 100644 index 000000000..b24b74add --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/AppKit/_NSConstraintBasedLayoutHostingView.h @@ -0,0 +1,59 @@ +// +// _NSConstraintBasedLayoutHostingView.h +// OpenSwiftUI_SPI +// +// Generated by `ipsw class-dump /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e 'AppKit' --headers` + +// +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.621, BuildCommit: Homebrew) +// +// - LC_BUILD_VERSION: Platform: macOS, MinOS: 15.5, SDK: 15.5, Tool: ld (1167.3) +// - LC_BUILD_VERSION: Platform: macCatalyst, MinOS: 18.5, SDK: 18.5, Tool: ld (1167.3) +// - LC_SOURCE_VERSION: 2575.60.5.0.0 +// +#ifndef _NSConstraintBasedLayoutHostingView_h +#define _NSConstraintBasedLayoutHostingView_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#include + +typedef struct BaselineOffset { + CGFloat firstTextBaseline; + CGFloat lastTextBaseline; +} BaselineOffset; + +@interface _NSConstraintBasedLayoutHostingView : NSView { + /* instance variables */ + BOOL _hasAddedConstraints; +} + +@property (retain, nullable) NSView *hostedView; + +/* class methods */ ++ (BOOL)requiresConstraintBasedLayout; + +/* instance methods */ +- (void)dealloc; +- (BaselineOffset)_baselineOffsetsAtSize:(CGSize)size; +- (void)_informContainerThatSubviewsNeedUpdateConstraints; +- (void)_intrinsicContentSizeInvalidatedForChildView:(id)view; +- (BOOL)_layoutHeightDependsOnWidth; +- (void)_layoutMetricsInvalidatedForHostedView; +- (CGSize)_layoutSizeThatFits:(CGSize)fits fixedAxes:(unsigned long long)axes; +- (void)_setFrameWithAlignmentRect:(CGRect)rect; +- (NSEdgeInsets)alignmentRectInsets; +- (void)constraintsDidChangeInEngine:(id)engine; +- (id)initWithHostedView:(id)view; +- (CGSize)sizeThatFits:(CGSize)fits; +- (void)sizeToFit; +- (void)updateConstraints; +- (void)willRemoveSubview:(id)subview; + +@end + +#endif /* __has_include() */ + +#endif /* _NSConstraintBasedLayoutHostingView_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/NSLayoutConstraint_UIKitAdditions.h b/Sources/OpenSwiftUI_SPI/Shims/UIKit/NSLayoutConstraint_UIKitAdditions.h new file mode 100644 index 000000000..258d2db79 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/NSLayoutConstraint_UIKitAdditions.h @@ -0,0 +1,25 @@ +// +// NSLayoutConstraint_UIKitAdditions.h +// OpenSwiftUI_SPI + +#ifndef NSLayoutConstraint_UIKitAdditions_h +#define NSLayoutConstraint_UIKitAdditions_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#include + +@interface UIView (NSLayoutConstraint_UIKitAdditions) +@property (nonatomic, assign, readonly) BOOL _wantsConstraintBasedLayout; +- (void)_setHostsLayoutEngine:(BOOL)hostsLayoutEngine; +@end + +@interface UIViewController (NSLayoutConstraint_UIKitAdditions) +- (void)_setViewHostsLayoutEngine:(BOOL)hostsLayoutEngine; +@end + +#endif /* __has_include() */ + +#endif /* NSLayoutConstraint_UIKitAdditions_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h index 1fb6acbff..cf6202dcc 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h @@ -60,6 +60,6 @@ bool _UIUpdateAdaptiveRateNeeded(); OPENSWIFTUI_ASSUME_NONNULL_END -#endif /* UIKit.h */ +#endif /* __has_include() */ #endif /* UIKit_Private_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m index f4c559f6e..d38f60d51 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m @@ -82,4 +82,4 @@ - (NSObject *)_environmentWrapper_openswiftui_safe_wrapper { } @end -#endif /* UIKit.h */ +#endif /* __has_include() */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/_UIConstraintBasedLayoutHostingView.h b/Sources/OpenSwiftUI_SPI/Shims/UIKit/_UIConstraintBasedLayoutHostingView.h new file mode 100644 index 000000000..b6b94a16c --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/_UIConstraintBasedLayoutHostingView.h @@ -0,0 +1,56 @@ +// +// _UIConstraintBasedLayoutHostingView.swift +// OpenSwiftUI_SPI +// +// Generated by `ipsw class-dump UIKitCore --headers` + +// +// Generated by https://github.com/blacktop/ipsw (Version: 3.1.621, BuildCommit: Homebrew) +// +// - LC_BUILD_VERSION: Platform: iOsSimulator, MinOS: 18.5, SDK: 18.5, Tool: ld (1167.3) +// - LC_SOURCE_VERSION: 8506.1.101.0.0 +// +#ifndef _UIConstraintBasedLayoutHostingView_h +#define _UIConstraintBasedLayoutHostingView_h + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#include + +typedef struct BaselineOffset { + CGFloat firstTextBaseline; + CGFloat lastTextBaseline; +} BaselineOffset; + +@interface _UIConstraintBasedLayoutHostingView : UIView { + /* instance variables */ + BOOL _hasAddedConstraints; +} + +@property (retain, nonatomic, nullable) UIView *hostedView; + +/* class methods */ ++ (BOOL)requiresConstraintBasedLayout; + +/* instance methods */ +- (id)initWithHostedView:(id)view; +- (void)willRemoveSubview:(id)subview; +- (void)_setFrameWithAlignmentRect:(CGRect)rect; +- (void)_intrinsicContentSizeInvalidatedForChildView:(id)view; +- (void)constraintsDidChangeInEngine:(id)engine; +- (void)_scheduleUpdateConstraintsPassAsEngineHostNeedingLayout:(BOOL)layout; +- (void)_layoutMetricsInvalidatedForHostedView; +- (void)updateConstraints; +- (UIEdgeInsets)alignmentRectInsets; +- (BOOL)_layoutHeightDependsOnWidth; +- (BaselineOffset)_baselineOffsetsAtSize:(CGSize)size; +- (CGSize)sizeThatFits:(CGSize)fits; +- (CGSize)_layoutSizeThatFits:(CGSize)fits fixedAxes:(unsigned long long)axes; + +@end + +#endif /* __has_include() */ + +#endif /* _UIConstraintBasedLayoutHostingView_h */