diff --git a/Example/HostingExample/ViewController.swift b/Example/HostingExample/ViewController.swift index 95151bc66..44bbec402 100644 --- a/Example/HostingExample/ViewController.swift +++ b/Example/HostingExample/ViewController.swift @@ -66,8 +66,6 @@ class ViewController: NSViewController { struct ContentView: View { var body: some View { - FlowLayoutDemo() - .frame(width: 500) - .padding() + GeometryReaderExample() } } diff --git a/Example/OpenSwiftUIUITests/Layout/Geometry/GeometryReaderUITests.swift b/Example/OpenSwiftUIUITests/Layout/Geometry/GeometryReaderUITests.swift new file mode 100644 index 000000000..38563db42 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Layout/Geometry/GeometryReaderUITests.swift @@ -0,0 +1,49 @@ +// +// GeometryReaderUITests.swift +// OpenSwiftUIUITests + +import SnapshotTesting +import Testing + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct GeometryReaderUITests { + @Test + func centerView() { + struct ContentView: View { + var body: some View { + GeometryReader { proxy in + Color.blue + .frame( + width: proxy.size.width / 2, + height: proxy.size.height / 2, + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.yellow.opacity(0.3)) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func overlapView() { + struct ContentView: View { + var body: some View { + GeometryReader { geometry in + Color.blue + .frame( + width: geometry.size.width / 2, + height: geometry.size.height / 2 + ) + Color.red + .frame( + width: geometry.size.width / 3, + height: geometry.size.height / 3 + ) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Example/SharedExample/Layout/Geometry/GeometryReaderExample.swift b/Example/SharedExample/Layout/Geometry/GeometryReaderExample.swift new file mode 100644 index 000000000..e4c949291 --- /dev/null +++ b/Example/SharedExample/Layout/Geometry/GeometryReaderExample.swift @@ -0,0 +1,27 @@ +// +// GeometryReaderExample.swift +// SharedExample + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +// FIXME: SwiftUI yellow background will ignoreSafeArea while OpenSwiftUI will have it. +// See #474 +struct GeometryReaderExample: View { + var body: some View { + GeometryReader { geometry in + VStack { + Color.blue + .frame( + width: geometry.size.width / 2, + height: geometry.size.height / 2 + ) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.yellow.opacity(0.3)) + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift b/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift new file mode 100644 index 000000000..9eb9ee82c --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Geometry/GeometryReader.swift @@ -0,0 +1,306 @@ +// +// GeometryReader.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 7D6D22DF7076CCC1FC5284D8E2D1B049 (SwiftUICore) + +public import Foundation +package import OpenGraphShims +import OpenSwiftUI_SPI + +// MARK: - GeometryReader [WIP] + +/// A container view that defines its content as a function of its own size and +/// coordinate space. +/// +/// This view returns a flexible preferred size to its parent layout. +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct GeometryReader: View, UnaryView, PrimitiveView where Content: View { + public var content: (GeometryProxy) -> Content + + @inlinable + public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) { + self.content = content + } + + public nonisolated static func _makeView( + view: _GraphValue, + inputs: _ViewInputs, + ) -> _ViewOutputs { + var inputs = inputs + let child = Attribute(Child( + view: view.value, + size: inputs.size, + position: inputs.position, + transform: inputs.transform, + environment: inputs.environment, + safeAreaInsets: inputs.safeAreaInsets, + seed: .zero + )) + var geometry: Attribute! + if inputs.needsGeometry { + let rootGeometry = Attribute(RootGeometry( + layoutDirection: .init(inputs.layoutDirection), + proposedSize: inputs.size + )) + inputs.position = Attribute(LayoutPositionQuery( + parentPosition: inputs.position, + localPosition: rootGeometry.origin() + )) + inputs.size = rootGeometry.size() + geometry = rootGeometry + } + var outputs = _VariadicView.Tree._makeView( + view: .init(child), + inputs: inputs + ) + if inputs.needsGeometry { + geometry.mutateBody(as: RootGeometry.self, invalidating: true) { geometry in + geometry.$childLayoutComputer = outputs.layoutComputer + } + } + outputs.layoutComputer = nil + return outputs + } + + private struct Child: StatefulRule, AsyncAttribute { + @Attribute var view: GeometryReader + @Attribute var size: ViewSize + @Attribute var position: ViewOrigin + @Attribute var transform: ViewTransform + @Attribute var environment: EnvironmentValues + @OptionalAttribute var safeAreaInsets: SafeAreaInsets? + var seed: UInt32 + + typealias Value = _VariadicView.Tree<_LayoutRoot, Content> + + mutating func updateValue() { + seed &+= 1 + let proxy = GeometryProxy( + owner: .current!, + size: $size, + environment: $environment, + transform: $transform, + position: $position, + safeAreaInsets: $safeAreaInsets, + seed: seed, + ) + // TODO: Observation + let content = view.content(proxy) + value = .init(root: .init(GeometryReaderLayout()), content: content) + } + } +} + +@available(*, unavailable) +extension GeometryReader: Sendable {} + +// MARK: - GeometryProxy [WIP] + +/// A proxy for access to the size and coordinate space (for anchor resolution) +/// of the container view. +@available(OpenSwiftUI_v1_0, *) +public struct GeometryProxy { + var owner: AnyWeakAttribute + var _size: WeakAttribute + var _environment: WeakAttribute + var _transform: WeakAttribute + var _position: WeakAttribute + var _safeAreaInsets: WeakAttribute + var seed: UInt32 + + package init( + owner: AnyAttribute, + size: Attribute, + environment: Attribute, + transform: Attribute, + position: Attribute, + safeAreaInsets: Attribute?, + seed: UInt32, + ) { + self.owner = AnyWeakAttribute(owner) + self._size = WeakAttribute(size) + self._environment = WeakAttribute(environment) + self._transform = WeakAttribute(transform) + self._position = WeakAttribute(position) + self._safeAreaInsets = WeakAttribute(safeAreaInsets) + self.seed = seed + } + + package var context: AnyRuleContext { + AnyRuleContext(attribute: owner.attribute ?? .nil) + } + + /// The size of the container view. + public var size: CGSize { + Update.perform { + guard let size = _size.attribute else { + return .zero + } + return context[size].value + } + } + + private var placementContext: _PositionAwarePlacementContext? { + Update.assertIsLocked() + guard let owner = owner.attribute, + let size = _size.attribute, + let environment = _environment.attribute, + let transform = _transform.attribute, + let position = _position.attribute + else { + return nil + } + return _PositionAwarePlacementContext( + context: .init(attribute: owner), + size: size, + environment: environment, + transform: transform, + position: position, + safeAreaInsets: .init(_safeAreaInsets.attribute), + ) + } + + /// Resolves the value of an anchor to the container view. + public subscript(anchor: Anchor) -> T { + _openSwiftUIUnimplementedFailure() + } + + /// The safe area inset of the container view. + public var safeAreaInsets: EdgeInsets { + Update.perform { + guard let placementContext else { + return .zero + } + return placementContext.safeAreaInsets() + } + } + + /// Returns the container view's bounds rectangle, converted to a defined + /// coordinate space. + @available(OpenSwiftUI_v1_0, *) + @available(*, deprecated, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @_disfavoredOverload + public func frame(in coordinateSpace: CoordinateSpace) -> CGRect { + let size = size + return Update.perform { + guard let placementContext else { + return .zero + } + var rect = CGRect(origin: .zero, size: size) + rect.convert(from: placementContext, to: coordinateSpace) + return rect + } + } + + @_spi(Private) + public func frameClippedToScrollViews(in space: CoordinateSpace) -> (frame: CGRect, exact: Bool) { + _openSwiftUIUnimplementedFailure() + } + + package func rect(_ r: CGRect, in coordinateSpace: CoordinateSpace) -> CGRect { + _openSwiftUIUnimplementedFailure() + } + + package var transform: ViewTransform { + _openSwiftUIUnimplementedFailure() + } + + package var environment: EnvironmentValues { + Update.perform { + guard let environment = _environment.attribute else { + return EnvironmentValues() + } + return context[environment] + } + } + + package static var current: GeometryProxy? { + if let data = _threadGeometryProxyData() { + data.assumingMemoryBound(to: GeometryProxy.self).pointee + } else { + nil + } + } + + package func asCurrent(do body: () throws -> Result) rethrows -> Result { + let old = _threadGeometryProxyData() + defer { _setThreadGeometryProxyData(old) } + return try withUnsafePointer(to: self) { ptr in + _setThreadGeometryProxyData(.init(mutating: ptr)) + return try body() + } + } +} + +@available(OpenSwiftUI_v5_0, *) +extension GeometryProxy { + /// Returns the given coordinate space's bounds rectangle, converted to the + /// local coordinate space. + public func bounds(of coordinateSpace: NamedCoordinateSpace) -> CGRect? { + _openSwiftUIUnimplementedFailure() + } + + /// Returns the container view's bounds rectangle, converted to a defined + /// coordinate space. + public func frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRect { + _openSwiftUIUnimplementedFailure() + } +} + +@available(OpenSwiftUI_v6_0, *) +extension GeometryProxy { + package func convert( + globalPoint: CGPoint, + to coordinateSpace: some CoordinateSpaceProtocol, + ) -> CGPoint { + _openSwiftUIUnimplementedFailure() + } +} + +@available(*, unavailable) +extension GeometryProxy: Sendable {} + +// MARK: - GeometryReaderLayout + +private struct GeometryReaderLayout: Layout { + static var layoutProperties: LayoutProperties { + var properties = LayoutProperties() + if !isLinkedOnOrAfter(.v2) { + properties.isDefaultEmptyLayout = true + properties.isIdentityUnaryLayout = true + } + return properties + } + + func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout (), + ) -> CGSize { + proposal.replacingUnspecifiedDimensions(by: .zero) + } + + func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout (), + ) { + guard !subviews.isEmpty else { + return + } + let anchor = UnitPoint.topLeading + for subview in subviews { + let dimensions = subview.dimensions(in: .init(bounds.size)) + subview.place( + at: bounds.origin, + anchor: anchor, + dimensions: dimensions, + ) + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/GeometryReader.swift b/Sources/OpenSwiftUICore/Layout/GeometryReader.swift deleted file mode 100644 index 2c46bb7ac..000000000 --- a/Sources/OpenSwiftUICore/Layout/GeometryReader.swift +++ /dev/null @@ -1,5 +0,0 @@ -// -// GeometryReader.swift -// OpenSwiftUICore -// -// Status: Empty diff --git a/Sources/OpenSwiftUICore/Layout/Layout.swift b/Sources/OpenSwiftUICore/Layout/Layout.swift index 053eef84c..a841a12c4 100644 --- a/Sources/OpenSwiftUICore/Layout/Layout.swift +++ b/Sources/OpenSwiftUICore/Layout/Layout.swift @@ -1734,7 +1734,13 @@ private struct PlacementData { let frame: CGRect let layoutDirection: LayoutDirection - static var current: UnsafeMutablePointer? { placementData } + static var current: PlacementData? { + if let placementData { + placementData.pointee + } else { + nil + } + } mutating func setGeometry(_ geometry: ViewGeometry, at index: Int, layoutDirection: LayoutDirection) { if geometrys[index].isInvalid { @@ -1765,12 +1771,18 @@ private struct AlignmentData { let ptr: UnsafeMutableRawPointer let viewSize: ViewSize - static var current: UnsafeMutablePointer? { alignmentData } + static var current: AlignmentData? { + if let alignmentData { + alignmentData.pointee + } else { + nil + } + } func asCurrent(do body: () throws -> Result) rethrows -> Result { - try withUnsafePointer(to: self) { ptr in - let oldData = alignmentData - defer { alignmentData = oldData } + let oldData = alignmentData + defer { alignmentData = oldData } + return try withUnsafePointer(to: self) { ptr in alignmentData = .init(mutating: ptr) return try body() } diff --git a/Sources/OpenSwiftUI_SPI/Util/TLS.h b/Sources/OpenSwiftUI_SPI/Util/TLS.h index 16e5321b8..460b9f368 100644 --- a/Sources/OpenSwiftUI_SPI/Util/TLS.h +++ b/Sources/OpenSwiftUI_SPI/Util/TLS.h @@ -11,17 +11,16 @@ #include "OpenSwiftUIBase.h" OPENSWIFTUI_EXPORT -void _setThreadGeometryProxyData(void * _Nullable data); +void _setThreadGeometryProxyData(void *_Nullable data); OPENSWIFTUI_EXPORT -void * _Nullable _threadGeometryProxyData(void); +void *_Nullable _threadGeometryProxyData(void); OPENSWIFTUI_EXPORT uint32_t _threadTransactionID(bool increase); - OPENSWIFTUI_EXPORT -void _setThreadTransactionData(void * _Nullable data); +void _setThreadTransactionData(void * _Nullable data); OPENSWIFTUI_EXPORT void * _Nullable _threadTransactionData(void);