From 24833fd1be0cee28c8fd3bdb482317bea4df210e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Feb 2026 18:52:41 +0800 Subject: [PATCH 1/2] Add ShadowEffect support (#776) Add _ShadowEffect as an EnvironmentalModifier with a _Resolved inner type conforming to RendererEffect. Includes View.shadow(color:radius:x:y:) convenience modifier. --- .../Render/RendererEffect/ShadowEffect.swift | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Render/RendererEffect/ShadowEffect.swift diff --git a/Sources/OpenSwiftUICore/Render/RendererEffect/ShadowEffect.swift b/Sources/OpenSwiftUICore/Render/RendererEffect/ShadowEffect.swift new file mode 100644 index 000000000..ad476194a --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/RendererEffect/ShadowEffect.swift @@ -0,0 +1,167 @@ +// +// ShadowEffect.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation + +// MARK: - _ShadowEffect + +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _ShadowEffect: EnvironmentalModifier, Equatable { + public var color: Color + + public var radius: CGFloat + + public var offset: CGSize + + @inlinable + public init(color: Color, radius: CGFloat, offset: CGSize) { + self.color = color + self.radius = radius + self.offset = offset + } + + public func resolve(in environment: EnvironmentValues) -> _ShadowEffect._Resolved { + _Resolved(style: ResolvedShadowStyle( + color: color.resolve(in: environment), + radius: radius, + offset: offset + )) + } + + @available(OpenSwiftUI_v4_0, *) + public static var _requiresMainThread: Bool { + false + } + + @usableFromInline + internal var _requiresMainThread: Bool { + false + } + + // MARK: - _Resolved + + public struct _Resolved: RendererEffect { + package var style: ResolvedShadowStyle + + public typealias AnimatableData = AnimatablePair>>, AnimatablePair> + + public var animatableData: AnimatableData { + get { style.animatableData } + set { style.animatableData = newValue } + } + + package func effectValue(size: CGSize) -> DisplayList.Effect { + .filter(.shadow(style)) + } + + public typealias Body = Never + } + + nonisolated public static func == (a: _ShadowEffect, b: _ShadowEffect) -> Bool { + a.color == b.color && a.radius == b.radius && a.offset == b.offset + } + + public typealias Body = Never + + public typealias ResolvedModifier = _ShadowEffect._Resolved +} + +@available(*, unavailable) +extension _ShadowEffect: Sendable {} + +@available(*, unavailable) +extension _ShadowEffect._Resolved: Sendable {} + +// MARK: - View + shadow + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Adds a shadow to this view. + /// + /// Use this modifier to add a shadow of a specified color behind a view. + /// You can offset the shadow from its view independently in the horizontal + /// and vertical dimensions using the `x` and `y` parameters. You can also + /// blur the edges of the shadow using the `radius` parameter. Use a + /// radius of zero to create a sharp shadow. Larger radius values produce + /// softer shadows. + /// + /// The example below creates a grid of boxes with varying offsets and blur. + /// Each box displays its radius and offset values for reference. + /// + /// struct Shadow: View { + /// let steps = [0, 5, 10] + /// + /// var body: some View { + /// VStack(spacing: 50) { + /// ForEach(steps, id: \.self) { offset in + /// HStack(spacing: 50) { + /// ForEach(steps, id: \.self) { radius in + /// Color.blue + /// .shadow( + /// color: .primary, + /// radius: CGFloat(radius), + /// x: CGFloat(offset), y: CGFloat(offset)) + /// .overlay { + /// VStack { + /// Text("\(radius)") + /// Text("(\(offset), \(offset))") + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// + /// ![A three by three grid of blue boxes with shadows. + /// All the boxes display an integer that indicates the shadow's radius and + /// an ordered pair that indicates the shadow's offset. The boxes in the + /// first row show zero offset and have shadows directly below the box; + /// the boxes in the second row show an offset of five in both directions + /// and have shadows with a small offset toward the right and down; the + /// boxes in the third row show an offset of ten in both directions and have + /// shadows with a large offset toward the right and down. The boxes in + /// the first column show a radius of zero have shadows with sharp edges; + /// the boxes in the second column show a radius of five and have shadows + /// with slightly blurry edges; the boxes in the third column show a radius + /// of ten and have very blurry edges. Because the shadow of the box in the + /// upper left is both completely sharp and directly below the box, it isn't + /// visible.](View-shadow-1-iOS) + /// + /// The example above uses ``Color/primary`` as the color to make the + /// shadow easy to see for the purpose of illustration. In practice, + /// you might prefer something more subtle, like ``Color/gray-8j2b``. + /// If you don't specify a color, the method uses a semi-transparent + /// black. + /// + /// - Parameters: + /// - color: The shadow's color. + /// - radius: A measure of how much to blur the shadow. Larger values + /// result in more blur. + /// - x: An amount to offset the shadow horizontally from the view. + /// - y: An amount to offset the shadow vertically from the view. + /// + /// - Returns: A view that adds a shadow to this view. + @inlinable + nonisolated public func shadow( + color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), + radius: CGFloat, + x: CGFloat = 0, + y: CGFloat = 0 + ) -> some View { + return modifier( + _ShadowEffect( + color: color, + radius: radius, + offset: CGSize(width: x, height: y) + ) + ) + } +} From 5e0445ca41ed7042761e973d7942658fb01704d2 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Feb 2026 19:20:51 +0800 Subject: [PATCH 2/2] Add ShadowEffectUITests --- .../RendererEffect/ClipEffectUITests.swift | 8 ++-- .../RendererEffect/ShadowEffectUITests.swift | 46 +++++++++++++++++++ Example/TestingHost/TestingHostApp.swift | 3 -- 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 Example/OpenSwiftUIUITests/Render/RendererEffect/ShadowEffectUITests.swift diff --git a/Example/OpenSwiftUIUITests/Render/RendererEffect/ClipEffectUITests.swift b/Example/OpenSwiftUIUITests/Render/RendererEffect/ClipEffectUITests.swift index fabb43728..e99e9a00d 100644 --- a/Example/OpenSwiftUIUITests/Render/RendererEffect/ClipEffectUITests.swift +++ b/Example/OpenSwiftUIUITests/Render/RendererEffect/ClipEffectUITests.swift @@ -8,7 +8,7 @@ import SnapshotTesting @MainActor @Suite(.snapshots(record: .never, diffTool: diffTool)) struct ClipEffectUITests { - @Test(.disabled("Shape is not implemented correctly")) + @Test func clipShapeCircle() { struct ContentView: View { var body: some View { @@ -20,7 +20,7 @@ struct ClipEffectUITests { openSwiftUIAssertSnapshot(of: ContentView()) } - @Test(.disabled("Shape is not implemented correctly")) + @Test func clipShapeRoundedRectangle() { struct ContentView: View { var body: some View { @@ -32,7 +32,7 @@ struct ClipEffectUITests { openSwiftUIAssertSnapshot(of: ContentView()) } - @Test(.disabled("Shape is not implemented correctly")) + @Test func clipShapeCapsule() { struct ContentView: View { var body: some View { @@ -59,7 +59,7 @@ struct ClipEffectUITests { openSwiftUIAssertSnapshot(of: ContentView()) } - @Test(.disabled("Shape is not implemented correctly")) + @Test func clipShapeEllipse() { struct ContentView: View { var body: some View { diff --git a/Example/OpenSwiftUIUITests/Render/RendererEffect/ShadowEffectUITests.swift b/Example/OpenSwiftUIUITests/Render/RendererEffect/ShadowEffectUITests.swift new file mode 100644 index 000000000..641ec94e1 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Render/RendererEffect/ShadowEffectUITests.swift @@ -0,0 +1,46 @@ +// +// ShadowEffectUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct ShadowEffectUITests { + @Test + func shadowDefault() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 100, height: 100) + .shadow(radius: 10) + } + } + openSwiftUIAssertSnapshot(of: ContentView(), drawHierarchyInKeyWindow: true) + } + + @Test + func shadowCustomColor() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 100, height: 100) + .shadow(color: .red, radius: 10) + } + } + openSwiftUIAssertSnapshot(of: ContentView(), drawHierarchyInKeyWindow: true) + } + + @Test + func shadowOffset() { + struct ContentView: View { + var body: some View { + Color.blue + .frame(width: 100, height: 100) + .shadow(color: .black, radius: 5, x: 10, y: 10) + } + } + openSwiftUIAssertSnapshot(of: ContentView(), drawHierarchyInKeyWindow: true) + } +} diff --git a/Example/TestingHost/TestingHostApp.swift b/Example/TestingHost/TestingHostApp.swift index 0b619b24e..9b730ceb0 100644 --- a/Example/TestingHost/TestingHostApp.swift +++ b/Example/TestingHost/TestingHostApp.swift @@ -2,9 +2,6 @@ // TestingHostApp.swift // TestingHost -// FIXME: OpenSwiftUI does not set up key window correctly -// -> use HostingExample as OpenSwiftUIUITests's host temporary to add drawHierarchyInKeyWindow support - #if OPENSWIFTUI import OpenSwiftUI #else