diff --git a/Example/OpenSwiftUIUITests/Layout/Modifier/PaddingLayoutUITests.swift b/Example/OpenSwiftUIUITests/Layout/Modifier/PaddingLayoutUITests.swift new file mode 100644 index 000000000..faa8b0e99 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Layout/Modifier/PaddingLayoutUITests.swift @@ -0,0 +1,246 @@ +// +// PaddingLayoutUITests.swift +// OpenSwiftUIUITests + +import SnapshotTesting +import Testing + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct PaddingLayoutUITests { + // MARK: - Basic Padding + + @Test + func defaultPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding() + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func specificAmountPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(20) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func zeroPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(0) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + // MARK: - Edge-Specific Padding + + @Test + func topPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.top, 30) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func bottomPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.bottom, 30) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func leadingPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.leading, 30) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func trailingPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.trailing, 30) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func horizontalPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.horizontal, 40) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func verticalPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.vertical, 40) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func multipleEdgesPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding([.top, .trailing], 25) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + // MARK: - EdgeInsets Padding + + @Test + func edgeInsetsPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(EdgeInsets(top: 10, leading: 20, bottom: 30, trailing: 15)) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func asymmetricEdgeInsets() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(EdgeInsets(top: 5, leading: 50, bottom: 10, trailing: 5)) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + // MARK: - Nested Padding + + @Test + func nestedPadding() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(10) + .background(Color.green) + .padding(20) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func multiplePaddingEdges() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 50, height: 30) + .padding(.leading, 30) + .background(Color.green) + .padding(.top, 20) + .background(Color.red) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + // MARK: - Padding with HVStack + + @Test + func paddingInVStack() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Color.blue + .frame(height: 30) + .padding(.bottom, 20) + .background(Color.red) + + Color.green + .frame(height: 30) + .padding(.top, 15) + .background(Color.yellow) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func paddingInHStack() { + struct ContentView: View { + var body: some View { + HStack(spacing: 0) { + Color.blue + .frame(width: 50) + .padding(.trailing, 25) + .background(Color.red) + + Color.green + .frame(width: 50) + .padding(.leading, 15) + .background(Color.yellow) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Modifier/DefaultPadding.swift b/Sources/OpenSwiftUICore/Layout/Modifier/DefaultPadding.swift new file mode 100644 index 000000000..794c19ef0 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Modifier/DefaultPadding.swift @@ -0,0 +1,35 @@ +// +// DefaultPadding.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 47C1BD8C61550BB60F4F3D12F752D53D (SwiftUICore) + +private struct DefaultPaddingKey: EnvironmentKey { + static let defaultValue: EdgeInsets = .init(_all: 16.0) +} + +@available(OpenSwiftUI_v3_0, *) +extension EnvironmentValues { + @_spi(_) + public var defaultPadding: EdgeInsets { + get { self[DefaultPaddingKey.self] } + set { self[DefaultPaddingKey.self] = newValue } + } +} + +@available(OpenSwiftUI_v2_0, *) +extension View { + /// For use by children in containers to disable the automatic padding that + /// those containers apply. + public func _ignoresAutomaticPadding(_ ignoresPadding: Bool) -> some View { + _openSwiftUIUnimplementedFailure() + } + + /// Applies explicit padding to a view that allows being disabled by that + /// view using `_ignoresAutomaticPadding`. + public func _automaticPadding(_ edgeInsets: EdgeInsets? = nil) -> some View { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Modifier/PaddingLayout.swift b/Sources/OpenSwiftUICore/Layout/Modifier/PaddingLayout.swift index 0b0f1e8e1..1a6b3b4b2 100644 --- a/Sources/OpenSwiftUICore/Layout/Modifier/PaddingLayout.swift +++ b/Sources/OpenSwiftUICore/Layout/Modifier/PaddingLayout.swift @@ -1,15 +1,22 @@ // // PaddingLayout.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete +// ID: A5372118658F90C947BF499CB95E323D (SwiftUICore) public import Foundation +/// Pads a view by the specified amount or the default amount. +/// +/// Child sizing: Stretches to fill minus the given inset. +/// +/// Preferred size: Preferred size of child plus inset. @frozen -public struct _PaddingLayout: /* UnaryLayout, */ Animatable, PrimitiveViewModifier /* , MultiViewModifier */ { +public struct _PaddingLayout: UnaryLayout { public var edges: Edge.Set + public var insets: EdgeInsets? @inlinable @@ -18,27 +25,221 @@ public struct _PaddingLayout: /* UnaryLayout, */ Animatable, PrimitiveViewModifi self.insets = insets } - public typealias AnimatableData = EmptyAnimatableData - public typealias Body = Never + package func placement(of child: LayoutProxy, in context: PlacementContext) -> _Placement { + let sizeAndSpacingContext = SizeAndSpacingContext(context) + let effectiveInsets = effectiveInsets(in: sizeAndSpacingContext) + let newProposal = context.proposedSize.inset(by: effectiveInsets) + return _Placement(proposedSize: newProposal, at: .init(effectiveInsets.originOffset)) + } + + package func sizeThatFits(in proposedSize: _ProposedSize, context: SizeAndSpacingContext, child: LayoutProxy) -> CGSize { + let effectiveInsets = effectiveInsets(in: context) + let newProposal = proposedSize.inset(by: effectiveInsets) + let size = child.size(in: newProposal) + return size.outset(by: effectiveInsets) + } + + package func ignoresAutomaticPadding(child: LayoutProxy) -> Bool { + true + } + + package func spacing(in context: SizeAndSpacingContext, child: LayoutProxy) -> Spacing { + if Semantics.NoSpacingProjectedPadding.isEnabled { + var spacing = child.layoutComputer.spacing() + let effectiveInsets = effectiveInsets(in: context) + var edgeSet = Edge.Set() + if effectiveInsets.top != 0 { + edgeSet.insert(.top) + } + if effectiveInsets.leading != 0 { + edgeSet.insert(.leading) + } + if effectiveInsets.bottom != 0 { + edgeSet.insert(.bottom) + } + if effectiveInsets.trailing != 0 { + edgeSet.insert(.trailing) + } + if Semantics.StopProjectingAffectedSpacing.isEnabled { + spacing.reset(.init(edgeSet, layoutDirection: context.layoutDirection)) + } else { + if !edgeSet.isEmpty { + spacing.reset(.init(edgeSet, layoutDirection: context.layoutDirection)) + } + } + return spacing + } else { + return child.layoutComputer.spacing() + } + } + + private func effectiveInsets(in context: SizeAndSpacingContext) -> EdgeInsets { + (insets ?? context.defaultPadding).in(edges) + } } +@available(OpenSwiftUI_v1_0, *) extension View { + /// Adds a different padding amount to each edge of this view. + /// + /// Use this modifier to add a different amount of padding on each edge + /// of a view: + /// + /// VStack { + /// Text("Text padded by different amounts on each edge.") + /// .padding(EdgeInsets(top: 10, leading: 20, bottom: 40, trailing: 0)) + /// .border(.gray) + /// Text("Unpadded text for comparison.") + /// .border(.yellow) + /// } + /// + /// The order in which you apply modifiers matters. The example above + /// applies the padding before applying the border to ensure that the + /// border encompasses the padded region: + /// + /// ![A screenshot of two text strings arranged vertically, each surrounded + /// by a border, with a small space between the two borders. + /// The first string says Text padded by different amounts on each edge. + /// Its border is gray, and there are different amounts of space between + /// the string and its border on each edge: 40 points on the bottom, 10 + /// points on the top, 20 points on the leading edge, and no space on + /// the trailing edge. + /// The second string says Unpadded text for comparison. + /// Its border is yellow, and there's no space between the string + /// and its border.](View-padding-3-iOS) + /// + /// To pad a view on specific edges with equal padding for all padded + /// edges, use ``View/padding(_:_:)``. To pad all edges of a view + /// equally, use ``View/padding(_:)``. + /// + /// - Parameter insets: An ``EdgeInsets`` instance that contains + /// padding amounts for each edge. + /// + /// - Returns: A view that's padded by different amounts on each edge. @inlinable - public func padding(_ insets: EdgeInsets) -> some View { + nonisolated public func padding(_ insets: EdgeInsets) -> some View { modifier(_PaddingLayout(insets: insets)) } + /// Adds an equal padding amount to specific edges of this view. + /// + /// Use this modifier to add a specified amount of padding to one or more + /// edges of the view. Indicate the edges to pad by naming either a single + /// value from ``Edge/Set``, or by specifying an + /// [OptionSet](https://developer.apple.com/documentation/Swift/OptionSet) + /// that contains edge values: + /// + /// VStack { + /// Text("Text padded by 20 points on the bottom and trailing edges.") + /// .padding([.bottom, .trailing], 20) + /// .border(.gray) + /// Text("Unpadded text for comparison.") + /// .border(.yellow) + /// } + /// + /// The order in which you apply modifiers matters. The example above + /// applies the padding before applying the border to ensure that the + /// border encompasses the padded region: + /// + /// ![A screenshot of two text strings arranged vertically, each surrounded + /// by a border, with a small space between the two borders. + /// The first string says Text padded by 20 points + /// on the bottom and trailing edges. + /// Its border is gray, and there are 20 points of space between the bottom + /// and trailing edges of the string and its border. + /// There's no space between the string and the border on the other edges. + /// The second string says Unpadded text for comparison. + /// Its border is yellow, and there's no space between the string + /// and its border.](View-padding-2-iOS) + /// + /// You can omit either or both of the parameters. If you omit the `length`, + /// OpenSwiftUI uses a default amount of padding. If you + /// omit the `edges`, OpenSwiftUI applies the padding to all edges. Omit both + /// to add a default padding all the way around a view. OpenSwiftUI chooses a + /// default amount of padding that's appropriate for the platform and + /// the presentation context. + /// + /// VStack { + /// Text("Text with default padding.") + /// .padding() + /// .border(.gray) + /// Text("Unpadded text for comparison.") + /// .border(.yellow) + /// } + /// + /// The example above looks like this in iOS under typical conditions: + /// + /// ![A screenshot of two text strings arranged vertically, each surrounded + /// by a border, with a small space between the two borders. + /// The first string says Text with default padding. + /// Its border is gray, and there is padding on all sides + /// between the border and the string it encloses in an amount that's + /// similar to the height of the text. + /// The second string says Unpadded text for comparison. + /// Its border is yellow, and there's no space between the string + /// and its border.](View-padding-2a-iOS) + /// + /// To control the amount of padding independently for each edge, use + /// ``View/padding(_:)-6pgqq``. To pad all outside edges of a view by a + /// specified amount, use ``View/padding(_:)-68shk``. + /// + /// - Parameters: + /// - edges: The set of edges to pad for this view. The default + /// is ``Edge/Set/all``. + /// - length: An amount, given in points, to pad this view on the + /// specified edges. If you set the value to `nil`, OpenSwiftUI uses + /// a platform-specific default amount. The default value of this + /// parameter is `nil`. + /// + /// - Returns: A view that's padded by the specified amount on the + /// specified edges. @inlinable - public func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View { + nonisolated public func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View { let insets = length.map { EdgeInsets(_all: $0) } return modifier(_PaddingLayout(edges: edges, insets: insets)) } + /// Adds a specific padding amount to each edge of this view. + /// + /// Use this modifier to add padding all the way around a view. + /// + /// VStack { + /// Text("Text padded by 10 points on each edge.") + /// .padding(10) + /// .border(.gray) + /// Text("Unpadded text for comparison.") + /// .border(.yellow) + /// } + /// + /// The order in which you apply modifiers matters. The example above + /// applies the padding before applying the border to ensure that the + /// border encompasses the padded region: + /// + /// ![A screenshot of two text strings arranged vertically, each surrounded + /// by a border, with a small space between the two borders. + /// The first string says Text padded by 10 points on each edge. + /// Its border is gray, and there are 10 points of space on all sides + /// between the string and its border. + /// The second string says Unpadded text for comparison. + /// Its border is yellow, and there's no space between the string + /// and its border.](View-padding-1-iOS) + /// + /// To independently control the amount of padding for each edge, use + /// ``View/padding(_:)-6pgqq``. To pad a select set of edges by the + /// same amount, use ``View/padding(_:_:)``. + /// + /// - Parameter length: The amount, given in points, to pad this view on all + /// edges. + /// + /// - Returns: A view that's padded by the amount you specify. @inlinable - public func padding(_ length: CGFloat) -> some View { + nonisolated public func padding(_ length: CGFloat) -> some View { padding(.all, length) } + /// Pads this view along all edges by an amount that is tighter than the + /// usual default value. + @available(OpenSwiftUI_v2_0, *) public func _tightPadding() -> some View { padding(8.0) } diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/BackgroundModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/BackgroundModifier.swift index c11e5e9dd..5f4b71d3d 100644 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/BackgroundModifier.swift +++ b/Sources/OpenSwiftUICore/Modifier/ViewModifier/BackgroundModifier.swift @@ -44,6 +44,56 @@ extension _BackgroundModifier : Sendable {} // MARK: - View + Background [6.4.41] [WIP] +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Layers the given view behind this view. + /// + /// Use `background(_:alignment:)` when you need to place one view behind + /// another, with the background view optionally aligned with a specified + /// edge of the frontmost view. + /// + /// The example below creates two views: the `Frontmost` view, and the + /// `DiamondBackground` view. The `Frontmost` view uses the + /// `DiamondBackground` view for the background of the image element inside + /// the `Frontmost` view's ``VStack``. + /// + /// struct DiamondBackground: View { + /// var body: some View { + /// VStack { + /// Rectangle() + /// .fill(.gray) + /// .frame(width: 250, height: 250, alignment: .center) + /// .rotationEffect(.degrees(45.0)) + /// } + /// } + /// } + /// + /// struct Frontmost: View { + /// var body: some View { + /// VStack { + /// Image(systemName: "folder") + /// .font(.system(size: 128, weight: .ultraLight)) + /// .background(DiamondBackground()) + /// } + /// } + /// } + /// + /// ![A view showing a large folder image with a gray diamond placed behind + /// it as its background view.](View-background-1) + /// + /// - Parameters: + /// - background: The view to draw behind this view. + /// - alignment: The alignment with a default value of + /// ``Alignment/center`` that you use to position the background view. + @available(*, deprecated, message: "Use `background(alignment:content:)` instead.") + @inlinable + @_disfavoredOverload + nonisolated public func background(_ background: Background, alignment: Alignment = .center) -> some View where Background: View { + modifier(_BackgroundModifier(background: background, alignment: alignment)) + } + +} + @available(OpenSwiftUI_v3_0, *) extension View { /// Layers the views that you specify behind this view. diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/OverlayModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/OverlayModifier.swift index 0b923adfa..1beae4108 100644 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/OverlayModifier.swift +++ b/Sources/OpenSwiftUICore/Modifier/ViewModifier/OverlayModifier.swift @@ -89,6 +89,35 @@ extension _OverlayModifier: Sendable {} // MARK: - View + Overlay [6.4.41] [WIP] +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Layers a secondary view in front of this view. + /// + /// When you apply an overlay to a view, the original view continues to + /// provide the layout characteristics for the resulting view. In the + /// following example, the heart image is shown overlaid in front of, and + /// aligned to the bottom of the folder image. + /// + /// Image(systemName: "folder") + /// .font(.system(size: 55, weight: .thin)) + /// .overlay(Text("❤️"), alignment: .bottom) + /// + /// ![View showing placement of a heart overlaid onto a folder + /// icon.](View-overlay-1) + /// + /// - Parameters: + /// - overlay: The view to layer in front of this view. + /// - alignment: The alignment for `overlay` in relation to this view. + /// + /// - Returns: A view that layers `overlay` in front of the view. + @available(*, deprecated, message: "Use `overlay(alignment:content:)` instead.") + @inlinable + @_disfavoredOverload + nonisolated public func overlay(_ overlay: Overlay, alignment: Alignment = .center) -> some View where Overlay: View { + modifier(_OverlayModifier(overlay: overlay, alignment: alignment)) + } +} + @available(OpenSwiftUI_v3_0, *) extension View { /// Layers the views that you specify in front of this view.