diff --git a/Package.swift b/Package.swift index 32c172f6..5da4c70e 100644 --- a/Package.swift +++ b/Package.swift @@ -97,6 +97,18 @@ if linkCoreUI { let symbolLocatorCondition = envEnable("OPENGSWIFTUI_SYMBOL_LOCATOR", default: buildForDarwinPlatform) +// MARK: - [env] OPENSWIFTUI_OPENCOMBINE + +let openCombineCondition = envEnable("OPENSWIFTUI_OPENCOMBINE", default: !buildForDarwinPlatform) + +// MARK: - [env] OPENSWIFTUI_SWIFT_LOG + +let swiftLogCondition = envEnable("OPENSWIFTUI_SWIFT_LOG", default: !buildForDarwinPlatform) + +// MARK: - [env] OPENSWIFTUI_SWIFT_CRYPTO + +let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform) + // MARK: - [env] OPENGSWIFTUI_SWIFTUI_RENDER let swiftUIRenderCondition = envEnable("OPENSWIFTUI_SWIFTUI_RENDER", default: buildForDarwinPlatform) @@ -127,7 +139,7 @@ if warningsAsErrorsCondition { // MARK: - [env] OPENSWIFTUI_LIBRARY_EVOLUTION let libraryEvolutionCondition = envEnable("OPENSWIFTUI_LIBRARY_EVOLUTION", default: buildForDarwinPlatform) -if libraryEvolutionCondition { +if libraryEvolutionCondition && !openCombineCondition && !swiftLogCondition { // NOTE: -enable-library-evolution will cause module verify failure for `swift build`. // Either set OPENSWIFTUI_LIBRARY_EVOLUTION=0 or add `-Xswiftc -no-verify-emitted-module-interface` after `swift build` sharedSwiftSettings.append(.unsafeFlags(["-enable-library-evolution", "-no-verify-emitted-module-interface"])) @@ -326,6 +338,7 @@ let openSwiftUISymbolDualTestsTarget = Target.testTarget( "OpenSwiftUI", "OpenSwiftUITestsSupport", "OpenSwiftUISymbolDualTestsSupport", + .product(name: "Numerics", package: "swift-numerics"), ], exclude: ["README.md"], cSettings: sharedCSettings, @@ -502,7 +515,6 @@ if useLocalDeps { package.dependencies += dependencies } -let openCombineCondition = envEnable("OPENSWIFTUI_OPENCOMBINE", default: !buildForDarwinPlatform) if openCombineCondition { package.dependencies.append( .package(url: "https://github.com/OpenSwiftUIProject/OpenCombine.git", from: "0.15.0") @@ -511,7 +523,6 @@ if openCombineCondition { openSwiftUITarget.addOpenCombineSettings() } -let swiftLogCondition = envEnable("OPENSWIFTUI_SWIFT_LOG", default: !buildForDarwinPlatform) if swiftLogCondition { package.dependencies.append( .package(url: "https://github.com/apple/swift-log", from: "1.5.3") @@ -520,7 +531,6 @@ if swiftLogCondition { openSwiftUITarget.addSwiftLogSettings() } -let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform) if swiftCryptoCondition { package.dependencies.append( .package(url: "https://github.com/apple/swift-crypto.git", from: "3.8.0") diff --git a/Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift b/Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift index 2d24c306..06af66cb 100644 --- a/Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift +++ b/Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift @@ -2,7 +2,7 @@ // FrameLayout.swift // OpenSwiftUICore // -// Status: WIP +// Status: Complete // ID: 73C64038119BBD0A6D8557B14379A404 (SwiftUICore) public import Foundation @@ -162,8 +162,7 @@ extension View { /// - Returns: A view with fixed dimensions of `width` and `height`, for the /// parameters that are non-`nil`. @inlinable - nonisolated - public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View { + nonisolated public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View { return modifier( _FrameLayout(width: width, height: height, alignment: alignment) ) @@ -182,4 +181,279 @@ extension View { } } -// MARK: - FlexFrameLayout [6.4.41] [WIP] +// MARK: - FlexFrameLayout [6.4.41] + +/// A modifier that aligns its child in an invisible, flexible frame with size +/// limits and ideal size properties. +@frozen +public struct _FlexFrameLayout: UnaryLayout, FrameLayoutCommon { + let minWidth: CGFloat? + let idealWidth: CGFloat? + let maxWidth: CGFloat? + let minHeight: CGFloat? + let idealHeight: CGFloat? + let maxHeight: CGFloat? + let alignment: Alignment + + /// Creates an instance with the given properties. + @usableFromInline + package init( + minWidth: CGFloat? = nil, + idealWidth: CGFloat? = nil, + maxWidth: CGFloat? = nil, + minHeight: CGFloat? = nil, + idealHeight: CGFloat? = nil, + maxHeight: CGFloat? = nil, + alignment: Alignment + ) { + let minW: CGFloat? = if let minWidth { + max(minWidth, .zero) + } else { + nil + } + let ideaW: CGFloat? = if let idealWidth { + max(minW ?? .zero, idealWidth) + } else { + nil + } + let maxW: CGFloat? = if let maxWidth { + max(ideaW ?? .zero, maxWidth) + } else { + nil + } + let minH: CGFloat? = if let minHeight { + max(minHeight, .zero) + } else { + nil + } + let ideaH: CGFloat? = if let idealHeight { + max(minH ?? .zero, idealHeight) + } else { + nil + } + let maxH: CGFloat? = if let maxHeight { + max(ideaH ?? .zero, maxHeight) + } else { + nil + } + let hasInvalidWidth = (minWidth ?? .zero) > (idealWidth ?? maxWidth ?? .infinity) || + (idealWidth ?? .zero) > (maxWidth ?? .infinity) || + (minWidth ?? .zero).isInfinite || (minWidth ?? .zero).isNaN + + let hasInvalidHeight = (minHeight ?? .zero) > (idealHeight ?? maxHeight ?? .infinity) || + (idealHeight ?? 0.0) > (maxHeight ?? .infinity) || + (minHeight ?? .zero).isInfinite || (minHeight ?? .zero).isNaN + + if (hasInvalidWidth || hasInvalidHeight) && isLinkedOnOrAfter(.v2) { + Log.runtimeIssues("Invalid frame dimension (negative or non-finite).") + } + + self.minWidth = minW + self.idealWidth = ideaW + self.maxWidth = maxW + self.minHeight = minH + self.idealHeight = ideaH + self.maxHeight = maxH + self.alignment = alignment + } + + private func childProposal(myProposal: _ProposedSize) -> _ProposedSize { + let width: CGFloat? = if let idealWidth { + min(max(myProposal.width ?? idealWidth, minWidth ?? -.infinity), maxWidth ?? .infinity) + } else { + nil + } + let height: CGFloat? = if let idealHeight { + min(max(myProposal.height ?? idealHeight, minHeight ?? -.infinity), maxHeight ?? .infinity) + } else { + nil + } + return _ProposedSize(width: width, height: height) + } + + package func sizeThatFits( + in proposedSize: _ProposedSize, + context: SizeAndSpacingContext, + child: LayoutProxy + ) -> CGSize { + let width: CGFloat? = if let width = proposedSize.width { + if let minWidth, let maxWidth, minWidth <= maxWidth { + min(max(width, minWidth), maxWidth) + } else { + nil + } + } else { + idealWidth + } + let height: CGFloat? = if let height = proposedSize.height { + if let minHeight, let maxHeight, minHeight <= maxHeight { + min(max(height, minHeight), maxHeight) + } else { + nil + } + } else { + idealHeight + } + guard let width, let height else { + let childProposal = childProposal(myProposal: proposedSize) + let size = child.size(in: childProposal) + + let finalWidth = if let width { + width + } else { + switch (minWidth, maxWidth) { + case let (minW?, maxW?) where minW <= maxW: + min(max(minW, size.width), maxW) + case let (minW?, nil): + max(min(childProposal.width ?? .infinity, size.width), minW) + case let (nil, maxW?): + min(max(childProposal.width ?? -.infinity, size.width), maxW) + default: + size.width + } + } + let finalHeight = if let height { + height + } else { + switch (minHeight, maxHeight) { + case let (minH?, maxH?) where minH <= maxH: + min(max(minH, size.height), maxH) + case let (minH?, nil): + max(min(childProposal.height ?? .infinity, size.height), minH) + case let (nil, maxH?): + min(max(childProposal.height ?? -.infinity, size.height), maxH) + default: + size.height + } + } + return CGSize(width: finalWidth, height: finalHeight) + } + return CGSize(width: width, height: height) + } + + private func childPlacementProposal(of child: LayoutProxy, context: PlacementContext) -> _ProposedSize { + func proposedDimension( + _ axis: Axis, + min: CGFloat? = nil, + ideal: CGFloat? = nil, + max: CGFloat? = nil + ) -> CGFloat? { + let value = context.size[axis] + guard ideal == nil, + context.proposedSize[axis] == nil, + (min ?? -.infinity) < value, value < (max ?? .infinity) + else { + return value + } + return nil + } + return _ProposedSize( + width: proposedDimension(.horizontal, min: minWidth, ideal: idealWidth, max: maxWidth), + height: proposedDimension(.vertical, min: minHeight, ideal: idealHeight, max: maxHeight) + ) + } + + package func placement(of child: LayoutProxy, in context: PlacementContext) -> _Placement { + let childProposal = if Semantics.FlexFrameIdealSizing.isEnabled { + childPlacementProposal(of: child, context: context) + } else { + _ProposedSize(context.size) + } + return commonPlacement(of: child, in: context, childProposal: childProposal) + } + + package func spacing(in context: SizeAndSpacingContext, child: LayoutProxy) -> Spacing { + if _SemanticFeature_v3.isEnabled, !child.requiresSpacingProjection { + var spacing = child.layoutComputer.spacing() + var edges: Edge.Set = [] + if minHeight != nil || idealHeight != nil || maxHeight != nil { + edges.formUnion(.vertical) + } + if minWidth != nil || idealWidth != nil || maxWidth != nil { + edges.formUnion(.horizontal) + } + spacing.reset(.init(edges, layoutDirection: context.layoutDirection)) + return spacing + } else { + return child.layoutComputer.spacing() + } + } +} + +extension View { + /// Positions this view within an invisible frame having the specified size + /// constraints. + /// + /// Always specify at least one size characteristic when calling this + /// method. Pass `nil` or leave out a characteristic to indicate that the + /// frame should adopt this view's sizing behavior, constrained by the other + /// non-`nil` arguments. + /// + /// The size proposed to this view is the size proposed to the frame, + /// limited by any constraints specified, and with any ideal dimensions + /// specified replacing any corresponding unspecified dimensions in the + /// proposal. + /// + /// If no minimum or maximum constraint is specified in a given dimension, + /// the frame adopts the sizing behavior of its child in that dimension. If + /// both constraints are specified in a dimension, the frame unconditionally + /// adopts the size proposed for it, clamped to the constraints. Otherwise, + /// the size of the frame in either dimension is: + /// + /// - If a minimum constraint is specified and the size proposed for the + /// frame by the parent is less than the size of this view, the proposed + /// size, clamped to that minimum. + /// - If a maximum constraint is specified and the size proposed for the + /// frame by the parent is greater than the size of this view, the + /// proposed size, clamped to that maximum. + /// - Otherwise, the size of this view. + /// + /// - Parameters: + /// - minWidth: The minimum width of the resulting frame. + /// - idealWidth: The ideal width of the resulting frame. + /// - maxWidth: The maximum width of the resulting frame. + /// - minHeight: The minimum height of the resulting frame. + /// - idealHeight: The ideal height of the resulting frame. + /// - maxHeight: The maximum height of the resulting frame. + /// - alignment: The alignment of this view inside the resulting frame. + /// Note that most alignment values have no apparent effect when the + /// size of the frame happens to match that of this view. + /// + /// - Returns: A view with flexible dimensions given by the call's non-`nil` + /// parameters. + @inlinable + nonisolated public func frame( + minWidth: CGFloat? = nil, + idealWidth: CGFloat? = nil, + maxWidth: CGFloat? = nil, + minHeight: CGFloat? = nil, + idealHeight: CGFloat? = nil, + maxHeight: CGFloat? = nil, + alignment: Alignment = .center + ) -> some View { + func areInNondecreasingOrder( + _ min: CGFloat?, _ ideal: CGFloat?, _ max: CGFloat? + ) -> Bool { + let min = min ?? -.infinity + let ideal = ideal ?? min + let max = max ?? ideal + return min <= ideal && ideal <= max + } + + if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth) + || !areInNondecreasingOrder(minHeight, idealHeight, maxHeight) + { + Log.runtimeIssues("Contradictory frame constraints specified.") + } + + return modifier( + _FlexFrameLayout( + minWidth: minWidth, + idealWidth: idealWidth, maxWidth: maxWidth, + minHeight: minHeight, + idealHeight: idealHeight, maxHeight: maxHeight, + alignment: alignment + ) + ) + } +} diff --git a/Sources/OpenSwiftUICore/Log/Logging.swift b/Sources/OpenSwiftUICore/Log/Logging.swift index 8c605f5b..26757bea 100644 --- a/Sources/OpenSwiftUICore/Log/Logging.swift +++ b/Sources/OpenSwiftUICore/Log/Logging.swift @@ -5,7 +5,12 @@ // Audited for iOS 18.0 // Status: Complete +#if DEBUG import Foundation +#else +public import Foundation +#endif + #if OPENSWIFTUI_SWIFT_LOG public import Logging extension Logger { @@ -19,6 +24,7 @@ extension Logger { public import os.log #if DEBUG +@usableFromInline package let dso = { () -> UnsafeMutableRawPointer in let count = _dyld_image_count() for i in 0 ..< count { @@ -110,6 +116,7 @@ package enum Log { package static var runtimeIssuesLog = Logger(subsystem: "com.apple.runtime-issues", category: "OpenSwiftUI") @_transparent + @usableFromInline package static func runtimeIssues( _ message: @autoclosure () -> StaticString, _ args: @autoclosure () -> [CVarArg] = [] @@ -121,6 +128,7 @@ package enum Log { package static var runtimeIssuesLog: OSLog = OSLog(subsystem: "com.apple.runtime-issues", category: "OpenSwiftUI") @_transparent + @usableFromInline package static func runtimeIssues( _ message: @autoclosure () -> StaticString, _ args: @autoclosure () -> [CVarArg] = [] diff --git a/Sources/OpenSwiftUICore/Log/Signpost.swift b/Sources/OpenSwiftUICore/Log/Signpost.swift index 1fb80c4e..120eaa29 100644 --- a/Sources/OpenSwiftUICore/Log/Signpost.swift +++ b/Sources/OpenSwiftUICore/Log/Signpost.swift @@ -34,7 +34,7 @@ private let _signpostLog = OSLog(subsystem: Log.subsystem, category: "OpenSwiftU #endif package struct Signpost { - #if canImport(Darwin) + #if canImport(Darwin) && !OPENSWIFTUI_SWIFT_LOG package static let archiving = OSSignposter(logger: Log.archiving) package static let metaExtraction = OSSignposter(logger: Log.metadataExtraction) #endif diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Layout/Modifier/FrameLayoutTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Layout/Modifier/FrameLayoutTestsStub.c new file mode 100644 index 00000000..f926b9a5 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Layout/Modifier/FrameLayoutTestsStub.c @@ -0,0 +1,13 @@ +// +// FrameLayoutTestsStub.c +// OpenSwiftUISymbolDualTestsSupport + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_FlexFrameLayoutInit, SwiftUI, $s7SwiftUI16_FlexFrameLayoutV8minWidth05idealG003maxG00F6Height0hJ00iJ09alignmentAC12CoreGraphics7CGFloatVSg_A5nA9AlignmentVtcfC); + +#endif diff --git a/Tests/OpenSwiftUICoreTests/Layout/Modifier/FrameLayoutTests.swift b/Tests/OpenSwiftUICoreTests/Layout/Modifier/FrameLayoutTests.swift new file mode 100644 index 00000000..4e22474b --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Layout/Modifier/FrameLayoutTests.swift @@ -0,0 +1,163 @@ +// +// FrameLayoutTests.swift +// OpenSwiftUICoreTests + +import Numerics +@testable import OpenSwiftUICore +import Testing + +// MARK: - FlexFrameLayoutTests + +struct FlexFrameLayoutTests { + // MARK: - Initialization Tests + + @Test(arguments: [ + (10.0, 20.0, 30.0, 15.0, 25.0, 35.0, Alignment.center), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Alignment.topLeading), + (100.0, 200.0, 300.0, 50.0, 100.0, 150.0, Alignment.bottomTrailing), + ]) + func initializationWithValidConstraints( + minWidth: Double, idealWidth: Double, maxWidth: Double, + minHeight: Double, idealHeight: Double, maxHeight: Double, + alignment: Alignment + ) { + let layout = _FlexFrameLayout( + minWidth: minWidth, + idealWidth: idealWidth, + maxWidth: maxWidth, + minHeight: minHeight, + idealHeight: idealHeight, + maxHeight: maxHeight, + alignment: alignment + ) + + #expect(layout.minWidth?.isApproximatelyEqual(to: minWidth) == true) + #expect(layout.idealWidth?.isApproximatelyEqual(to: idealWidth) == true) + #expect(layout.maxWidth?.isApproximatelyEqual(to: maxWidth) == true) + #expect(layout.minHeight?.isApproximatelyEqual(to: minHeight) == true) + #expect(layout.idealHeight?.isApproximatelyEqual(to: idealHeight) == true) + #expect(layout.maxHeight?.isApproximatelyEqual(to: maxHeight) == true) + #expect(layout.alignment == alignment) + } + + @Test(arguments: [ + Alignment.center, + Alignment.topLeading, + Alignment.bottomTrailing, + Alignment.leading, + Alignment.trailing + ]) + func initializationWithNilValues(alignment: Alignment) { + let layout = _FlexFrameLayout(alignment: alignment) + + #expect(layout.minWidth == nil) + #expect(layout.idealWidth == nil) + #expect(layout.maxWidth == nil) + #expect(layout.minHeight == nil) + #expect(layout.idealHeight == nil) + #expect(layout.maxHeight == nil) + #expect(layout.alignment == alignment) + } + + @Test(arguments: [ + (-10.0, -5.0), + (-100.0, -50.0), + (-0.1, -0.001) + ]) + func initializationClampsNegativeMinValues(minWidth: Double, minHeight: Double) { + let layout = _FlexFrameLayout( + minWidth: minWidth, + minHeight: minHeight, + alignment: .center + ) + + #expect(layout.minWidth?.isApproximatelyEqual(to: 0) == true) + #expect(layout.minHeight?.isApproximatelyEqual(to: 0) == true) + } + + @Test(arguments: [ + (20.0, 10.0, 30.0, 15.0), + (50.0, 25.0, 100.0, 50.0), + (100.0, 0.0, 200.0, 0.0) + ]) + func initializationClampsIdealToMinimum( + minWidth: Double, idealWidth: Double, + minHeight: Double, idealHeight: Double + ) { + let layout = _FlexFrameLayout( + minWidth: minWidth, + idealWidth: idealWidth, + minHeight: minHeight, + idealHeight: idealHeight, + alignment: .center + ) + + #expect(layout.idealWidth?.isApproximatelyEqual(to: minWidth) == true) + #expect(layout.idealHeight?.isApproximatelyEqual(to: minHeight) == true) + } + + @Test(arguments: [ + (30.0, 20.0, 40.0, 25.0), + (100.0, 50.0, 200.0, 100.0), + (50.0, 25.0, 75.0, 50.0) + ]) + func initializationClampsMaxToIdeal( + idealWidth: Double, maxWidth: Double, + idealHeight: Double, maxHeight: Double + ) { + let layout = _FlexFrameLayout( + idealWidth: idealWidth, + maxWidth: maxWidth, + idealHeight: idealHeight, + maxHeight: maxHeight, + alignment: .center + ) + + #expect(layout.maxWidth?.isApproximatelyEqual(to: idealWidth) == true) + #expect(layout.maxHeight?.isApproximatelyEqual(to: idealHeight) == true) + } + + // MARK: - Corner Cases with Special Float Values + + @Test(arguments: [ + Double.infinity, + -Double.infinity, + Double.nan + ]) + func initializationWithInfinityAndNaNMinWidth(minWidth: Double) { + let layout = _FlexFrameLayout( + minWidth: minWidth, + alignment: .center + ) + if minWidth < 0 { + #expect(layout.minWidth?.isApproximatelyEqual(to: 0) == true) + } else if minWidth.isNaN { + #expect(layout.minWidth?.isNaN == true) + } else if minWidth.isInfinite { + #expect(layout.minWidth?.isInfinite == true) + } + #expect(layout.idealWidth == nil) + #expect(layout.maxWidth == nil) + } + + @Test(arguments: [ + Double.infinity, + -Double.infinity, + Double.nan + ]) + func initializationWithInfinityAndNaNMinHeight(minHeight: Double) { + let layout = _FlexFrameLayout( + minHeight: minHeight, + alignment: .center + ) + if minHeight < 0 { + #expect(layout.minHeight?.isApproximatelyEqual(to: 0) == true) + } else if minHeight.isNaN { + #expect(layout.minHeight?.isNaN == true) + } else if minHeight.isInfinite { + #expect(layout.minHeight?.isInfinite == true) + } + #expect(layout.idealHeight == nil) + #expect(layout.maxHeight == nil) + } +} diff --git a/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift b/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift index b0a3366f..8a7fb1ca 100644 --- a/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift +++ b/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift @@ -8,8 +8,6 @@ import SwiftUI import OpenSwiftUI import OpenSwiftUITestsSupport -#if compiler(>=6.1) // https://github.com/swiftlang/swift/issues/81248 - extension PropertyList { @_silgen_name("OpenSwiftUITestStub_PropertyListInit") init(swiftUI: Void) @@ -335,5 +333,3 @@ struct PropertyListTrackerTests { } #endif - -#endif diff --git a/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift b/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift index 39350b69..90343c73 100644 --- a/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift +++ b/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift @@ -5,16 +5,14 @@ #if canImport(SwiftUI, _underlyingVersion: 6.0.87) import Testing import SwiftUI -import OpenSwiftUI extension CGSize { - var swiftUI_hasZero: Bool { + var hasZero: Bool { @_silgen_name("OpenSwiftUITestStub_CGSizeHasZero") get } } -#if compiler(>=6.1) // https://github.com/swiftlang/swift/issues/81248 struct CGSize_ExtensionTests { @Test( .enabled { @@ -32,10 +30,9 @@ struct CGSize_ExtensionTests { ] ) func hasZero(size: CGSize, expectedResult: Bool) { - let result = size.swiftUI_hasZero + let result = size.hasZero #expect(result == expectedResult) } } -#endif #endif diff --git a/Tests/OpenSwiftUISymbolDualTests/Layout/Modifier/FrameLayoutTests.swift b/Tests/OpenSwiftUISymbolDualTests/Layout/Modifier/FrameLayoutTests.swift new file mode 100644 index 00000000..f1dc61fc --- /dev/null +++ b/Tests/OpenSwiftUISymbolDualTests/Layout/Modifier/FrameLayoutTests.swift @@ -0,0 +1,179 @@ +// +// FrameLayoutTests.swift +// OpenSwiftUICoreTests + +#if canImport(SwiftUI, _underlyingVersion: 6.0.87) +import Foundation +import Numerics +@testable import OpenSwiftUICore +import Testing + +// MARK: - FlexFrameLayoutTests + +extension _FlexFrameLayout { + @_silgen_name("OpenSwiftUITestStub_FlexFrameLayoutInit") + init( + swiftUI_minWidth: CGFloat? = nil, + swiftUI_idealWidth: CGFloat? = nil, + swiftUI_maxWidth: CGFloat? = nil, + swiftUI_minHeight: CGFloat? = nil, + swiftUI_idealHeight: CGFloat? = nil, + swiftUI_maxHeight: CGFloat? = nil, + swiftUI_alignment: Alignment + ) +} + +struct FlexFrameLayoutTests { + // MARK: - Initialization Tests + + @Test(arguments: [ + (10.0, 20.0, 30.0, 15.0, 25.0, 35.0, Alignment.center), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Alignment.topLeading), + (100.0, 200.0, 300.0, 50.0, 100.0, 150.0, Alignment.bottomTrailing), + ]) + func initializationWithValidConstraints( + minWidth: Double, idealWidth: Double, maxWidth: Double, + minHeight: Double, idealHeight: Double, maxHeight: Double, + alignment: Alignment + ) { + let layout = _FlexFrameLayout( + swiftUI_minWidth: minWidth, + swiftUI_idealWidth: idealWidth, + swiftUI_maxWidth: maxWidth, + swiftUI_minHeight: minHeight, + swiftUI_idealHeight: idealHeight, + swiftUI_maxHeight: maxHeight, + swiftUI_alignment: alignment + ) + + #expect(layout.minWidth?.isApproximatelyEqual(to: minWidth) == true) + #expect(layout.idealWidth?.isApproximatelyEqual(to: idealWidth) == true) + #expect(layout.maxWidth?.isApproximatelyEqual(to: maxWidth) == true) + #expect(layout.minHeight?.isApproximatelyEqual(to: minHeight) == true) + #expect(layout.idealHeight?.isApproximatelyEqual(to: idealHeight) == true) + #expect(layout.maxHeight?.isApproximatelyEqual(to: maxHeight) == true) + #expect(layout.alignment == alignment) + } + + @Test(arguments: [ + Alignment.center, + Alignment.topLeading, + Alignment.bottomTrailing, + Alignment.leading, + Alignment.trailing + ]) + func initializationWithNilValues(alignment: Alignment) { + let layout = _FlexFrameLayout(swiftUI_alignment: alignment) + + #expect(layout.minWidth == nil) + #expect(layout.idealWidth == nil) + #expect(layout.maxWidth == nil) + #expect(layout.minHeight == nil) + #expect(layout.idealHeight == nil) + #expect(layout.maxHeight == nil) + #expect(layout.alignment == alignment) + } + + @Test(arguments: [ + (-10.0, -5.0), + (-100.0, -50.0), + (-0.1, -0.001) + ]) + func initializationClampsNegativeMinValues(minWidth: Double, minHeight: Double) { + let layout = _FlexFrameLayout( + swiftUI_minWidth: minWidth, + swiftUI_minHeight: minHeight, + swiftUI_alignment: .center + ) + + #expect(layout.minWidth?.isApproximatelyEqual(to: 0) == true) + #expect(layout.minHeight?.isApproximatelyEqual(to: 0) == true) + } + + @Test(arguments: [ + (20.0, 10.0, 30.0, 15.0), + (50.0, 25.0, 100.0, 50.0), + (100.0, 0.0, 200.0, 0.0) + ]) + func initializationClampsIdealToMinimum( + minWidth: Double, idealWidth: Double, + minHeight: Double, idealHeight: Double + ) { + let layout = _FlexFrameLayout( + swiftUI_minWidth: minWidth, + swiftUI_idealWidth: idealWidth, + swiftUI_minHeight: minHeight, + swiftUI_idealHeight: idealHeight, + swiftUI_alignment: .center + ) + + #expect(layout.idealWidth?.isApproximatelyEqual(to: minWidth) == true) + #expect(layout.idealHeight?.isApproximatelyEqual(to: minHeight) == true) + } + + @Test(arguments: [ + (30.0, 20.0, 40.0, 25.0), + (100.0, 50.0, 200.0, 100.0), + (50.0, 25.0, 75.0, 50.0) + ]) + func initializationClampsMaxToIdeal( + idealWidth: Double, maxWidth: Double, + idealHeight: Double, maxHeight: Double + ) { + let layout = _FlexFrameLayout( + swiftUI_idealWidth: idealWidth, + swiftUI_maxWidth: maxWidth, + swiftUI_idealHeight: idealHeight, + swiftUI_maxHeight: maxHeight, + swiftUI_alignment: .center + ) + + #expect(layout.maxWidth?.isApproximatelyEqual(to: idealWidth) == true) + #expect(layout.maxHeight?.isApproximatelyEqual(to: idealHeight) == true) + } + + // MARK: - Corner Cases with Special Float Values + + @Test(arguments: [ + Double.infinity, + -Double.infinity, + Double.nan + ]) + func initializationWithInfinityAndNaNMinWidth(minWidth: Double) { + let layout = _FlexFrameLayout( + swiftUI_minWidth: minWidth, + swiftUI_alignment: .center + ) + if minWidth < 0 { + #expect(layout.minWidth?.isApproximatelyEqual(to: 0) == true) + } else if minWidth.isNaN { + #expect(layout.minWidth?.isNaN == true) + } else if minWidth.isInfinite { + #expect(layout.minWidth?.isInfinite == true) + } + #expect(layout.idealWidth == nil) + #expect(layout.maxWidth == nil) + } + + @Test(arguments: [ + Double.infinity, + -Double.infinity, + Double.nan + ]) + func initializationWithInfinityAndNaNMinHeight(minHeight: Double) { + let layout = _FlexFrameLayout( + swiftUI_minHeight: minHeight, + swiftUI_alignment: .center + ) + if minHeight < 0 { + #expect(layout.minHeight?.isApproximatelyEqual(to: 0) == true) + } else if minHeight.isNaN { + #expect(layout.minHeight?.isNaN == true) + } else if minHeight.isInfinite { + #expect(layout.minHeight?.isInfinite == true) + } + #expect(layout.idealHeight == nil) + #expect(layout.maxHeight == nil) + } +} +#endif