Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 0 additions & 3 deletions Example/TestingHost/TestingHostApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
167 changes: 167 additions & 0 deletions Sources/OpenSwiftUICore/Render/RendererEffect/ShadowEffect.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnvironmentalModifier._requiresMainThread is available starting OpenSwiftUI_v3_0, but this override is only available from OpenSwiftUI_v4_0, so on v3 the default true may still apply and prevent async updates. Also, the instance _requiresMainThread property doesn’t appear to be referenced elsewhere, so it might be dead/unintentional.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

false
}

@usableFromInline
internal var _requiresMainThread: Bool {
false
}

// MARK: - _Resolved

public struct _Resolved: RendererEffect {
package var style: ResolvedShadowStyle

public typealias AnimatableData = AnimatablePair<AnimatablePair<Float, AnimatablePair<Float, AnimatablePair<Float, Float>>>, AnimatablePair<CGFloat, CGSize.AnimatableData>>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_Resolved.AnimatableData is spelled out explicitly rather than using ResolvedShadowStyle.AnimatableData, which tightly couples this effect to the current internal shape of Color.Resolved.AnimatableData. If that underlying type changes, animations could break or this could stop compiling even though ResolvedShadowStyle still conforms to Animatable.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


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,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public shadow(... radius:) API forwards radius directly into ResolvedShadowStyle, but ResolvedShadowStyle.insets assumes radius is non-negative (it multiplies by -2.8). If callers pass a negative radius, this can produce unexpected insets/rendering, so it may be worth guarding or documenting the valid range.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

x: CGFloat = 0,
y: CGFloat = 0
) -> some View {
return modifier(
_ShadowEffect(
color: color,
radius: radius,
offset: CGSize(width: x, height: y)
)
)
}
}
Loading