Skip to content

Commit 4b362fc

Browse files
committed
feat(render): complete opacity effects
1 parent f6f0fd0 commit 4b362fc

2 files changed

Lines changed: 291 additions & 10 deletions

File tree

‎Sources/OpenSwiftUICore/Render/RendererEffect/OpacityEffect.swift‎

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
// OpenSwiftUICore
44
//
55
// Audited for 6.5.4
6-
// Status: WIP
6+
// Status: Complete
77
// ID: 34FFA2034B9AD53E0463E3971529C5A1 (SwiftUICore)
88

99
package import OpenCoreGraphicsShims
10-
import OpenAttributeGraphShims
10+
package import OpenAttributeGraphShims
1111

1212
// MARK: - _OpacityEffect
1313

@@ -55,6 +55,11 @@ public struct _OpacityEffect: RendererEffect, Equatable {
5555
)
5656
)
5757
}
58+
inputs.opacityAccessibilityProvider.makeOpacity(
59+
effect: modifier.value,
60+
inputs: inputs,
61+
outputs: &outputs
62+
)
5863
return outputs
5964
}
6065
}
@@ -93,7 +98,7 @@ extension View {
9398
///
9499
/// ![Two overlaid rectangles, where the topmost has its opacity set to 50%,
95100
/// which allows the occluded portion of the bottom rectangle to be
96-
/// visible.](OpenSwiftUI-View-opacity.png)
101+
/// visible.](OpenSwiftUI-View-opacity)
97102
///
98103
/// - Parameter opacity: A value between 0 (fully transparent) and 1 (fully
99104
/// opaque).
@@ -215,17 +220,141 @@ extension _OpacityEffect: ProtobufMessage {
215220

216221
// MARK: - ShapeStyle + _OpacityShapeStyle
217222

218-
// TODO: _OpacityShapeStyle
223+
@available(OpenSwiftUI_v4_0, *)
224+
extension ShapeStyle where Self == AnyShapeStyle {
225+
/// Returns a new style based on the current style that multiplies
226+
/// by `opacity` when drawing.
227+
///
228+
/// In most contexts the current style is the foreground but e.g.
229+
/// when setting the value of the background style, that becomes
230+
/// the current implicit style.
231+
///
232+
/// For example, a circle filled with the current foreground
233+
/// style at fifty-percent opacity:
234+
///
235+
/// Circle().fill(.opacity(0.5))
236+
///
237+
@_alwaysEmitIntoClient
238+
public static func opacity(_ opacity: Double) -> some ShapeStyle {
239+
_OpacityShapeStyle(style: _ImplicitShapeStyle(), opacity: Float(opacity))
240+
}
241+
}
242+
243+
// MARK: - _OpacityShapeStyle
244+
245+
@available(OpenSwiftUI_v3_0, *)
246+
@frozen
247+
public struct _OpacityShapeStyle<Style>: ShapeStyle, PrimitiveShapeStyle where Style: ShapeStyle {
248+
public var style: Style
249+
250+
public var opacity: Float
251+
252+
@inlinable
253+
public init(style: Style, opacity: Float) {
254+
self.style = style
255+
self.opacity = opacity
256+
}
257+
258+
public func _apply(to shape: inout _ShapeStyle_Shape) {
259+
guard opacity != 1 else {
260+
style._apply(to: &shape)
261+
return
262+
}
263+
switch shape.operation {
264+
case .prepareText:
265+
shape.result = .preparedText(.foregroundKeyColor)
266+
case let .resolveStyle(name, levels):
267+
style._apply(to: &shape)
268+
shape.stylePack.modify(name: name, levels: levels) { style in
269+
style.applyOpacity(opacity)
270+
}
271+
case .fallbackColor:
272+
style._apply(to: &shape)
273+
if case let .color(color) = shape.result {
274+
shape.result = .color(color.opacity(Double(opacity)))
275+
}
276+
case .copyStyle:
277+
style.mapCopiedStyle(in: &shape) { style in
278+
_OpacityShapeStyle<AnyShapeStyle>(style: style, opacity: opacity)
279+
}
280+
case .modifyBackground, .multiLevel:
281+
style._apply(to: &shape)
282+
case .primaryStyle:
283+
break
284+
}
285+
}
219286

220-
// TODO: _OpacitiesShapeStyle
287+
public static func _apply(to type: inout _ShapeStyle_ShapeType) {
288+
Style._apply(to: &type)
289+
}
290+
}
291+
292+
// MARK: - _OpacitiesShapeStyle
293+
294+
@_spi(Private)
295+
@available(OpenSwiftUI_v3_0, *)
296+
@frozen
297+
public struct _OpacitiesShapeStyle<Style>: ShapeStyle, PrimitiveShapeStyle where Style: ShapeStyle {
298+
public var style: Style
299+
300+
public var opacities: [Double]
301+
302+
@inlinable
303+
public init(style: Style, opacities: [Double]) {
304+
self.style = style
305+
self.opacities = opacities
306+
}
307+
308+
public func _apply(to shape: inout _ShapeStyle_Shape) {
309+
switch shape.operation {
310+
case .prepareText:
311+
shape.result = .preparedText(.foregroundKeyColor)
312+
case let .resolveStyle(name, levels):
313+
shape.operation = .resolveStyle(name: name, levels: 0..<1)
314+
style._apply(to: &shape)
315+
let style = shape.stylePack[name, 0]
316+
for level in levels {
317+
let index = min(level, opacities.count - 1)
318+
let opacity = index >= 0 ? opacities[index] : 1.0
319+
shape.stylePack[name, level] = style.applyingOpacity(Float(opacity))
320+
}
321+
case let .fallbackColor(level):
322+
shape.operation = .fallbackColor(level: 0)
323+
style._apply(to: &shape)
324+
if case let .color(color) = shape.result {
325+
let index = min(level, opacities.count - 1)
326+
let opacity = index >= 0 ? opacities[index] : 1.0
327+
shape.result = .color(color.opacity(opacity))
328+
}
329+
case .copyStyle:
330+
style.mapCopiedStyle(in: &shape) { style in
331+
_OpacitiesShapeStyle<AnyShapeStyle>(style: style, opacities: opacities)
332+
}
333+
case .modifyBackground:
334+
style._apply(to: &shape)
335+
case .multiLevel:
336+
shape.result = .bool(true)
337+
case .primaryStyle:
338+
break
339+
}
340+
}
341+
342+
public static func _apply(to type: inout _ShapeStyle_ShapeType) {
343+
Style._apply(to: &type)
344+
}
345+
}
221346

222347
// MARK: - OpacityTransition
223348

224349
/// A transition from transparent to opaque on insertion, and from opaque to
225350
/// transparent on removal.
226351
@available(OpenSwiftUI_v5_0, *)
352+
@MainActor
353+
@preconcurrency
227354
public struct OpacityTransition: Transition {
228-
public init() {}
355+
public init() {
356+
_openSwiftUIEmptyStub()
357+
}
229358

230359
public func body(
231360
content: OpacityTransition.Content,
@@ -276,7 +405,7 @@ struct OpacityResponderFilter: StatefulRule {
276405

277406
// MARK: - OpacityAccessibilityProvider
278407

279-
protocol OpacityAccessibilityProvider {
408+
package protocol OpacityAccessibilityProvider {
280409
static func makeOpacity(
281410
effect: @autoclosure () -> Attribute<_OpacityEffect>,
282411
inputs: _ViewInputs,
@@ -300,17 +429,17 @@ struct EmptyOpacityAccessibilityProvider: OpacityAccessibilityProvider {
300429

301430
extension _GraphInputs {
302431
private struct OpacityAccessibilityProviderKey: GraphInput {
303-
static let defaultValue: OpacityAccessibilityProvider.Type = EmptyOpacityAccessibilityProvider.self
432+
static let defaultValue: (any OpacityAccessibilityProvider.Type) = EmptyOpacityAccessibilityProvider.self
304433
}
305434

306-
var opacityAccessibilityProvider: OpacityAccessibilityProvider.Type {
435+
package var opacityAccessibilityProvider: (any OpacityAccessibilityProvider.Type) {
307436
get { self[OpacityAccessibilityProviderKey.self] }
308437
set { self[OpacityAccessibilityProviderKey.self] = newValue }
309438
}
310439
}
311440

312441
extension _ViewInputs {
313-
var opacityAccessibilityProvider: OpacityAccessibilityProvider.Type {
442+
package var opacityAccessibilityProvider: (any OpacityAccessibilityProvider.Type) {
314443
get { base.opacityAccessibilityProvider }
315444
set { base.opacityAccessibilityProvider = newValue }
316445
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// OpacityEffectTests.swift
3+
// OpenSwiftUICoreTests
4+
5+
@_spi(Private) @testable import OpenSwiftUICore
6+
import Testing
7+
import OpenSwiftUITestsSupport
8+
9+
@Suite(.tags(.aigc))
10+
struct OpacityEffectTests {
11+
@Test
12+
func opacityScalesOnlyRequestedStylesAndTheirEffects() {
13+
var shape = _ShapeStyle_Shape(operation: .resolveStyle(name: .foreground, levels: 1..<3))
14+
_OpacityShapeStyle(style: TestStyle(), opacity: 0.5)._apply(to: &shape)
15+
16+
#expect(shape.stylePack[.foreground, 0].opacity == 0.5)
17+
#expect(shape.stylePack[.foreground, 1].opacity == 0.25)
18+
#expect(shape.stylePack[.foreground, 2].opacity == 0.25)
19+
#expect(shape.stylePack[.foreground, 3].opacity == 0.5)
20+
#expect(shape.stylePack[.background, 0].opacity == 0.5)
21+
#expect(shape.stylePack[.foreground, 1].effects[0].opacity == 0.125)
22+
#expect(shape.stylePack[.foreground, 0].effects[0].opacity == 0.25)
23+
}
24+
25+
@Test(arguments: [Float(0), 0.5, -1, 2])
26+
func textRequiresKeyColorForNonidentityOpacity(opacity: Float) {
27+
var shape = _ShapeStyle_Shape(operation: .prepareText(level: 0))
28+
_OpacityShapeStyle(style: TestStyle(), opacity: opacity)._apply(to: &shape)
29+
30+
guard case .preparedText(.foregroundKeyColor) = shape.result else {
31+
Issue.record("Expected foreground key color")
32+
return
33+
}
34+
}
35+
36+
@Test
37+
func identityOpacityPreservesTextPreparation() {
38+
var shape = _ShapeStyle_Shape(operation: .prepareText(level: 0))
39+
_OpacityShapeStyle(style: TestStyle(), opacity: 1)._apply(to: &shape)
40+
41+
guard case .preparedText(.foregroundColor(.red)) = shape.result else {
42+
Issue.record("Expected the base style's text color")
43+
return
44+
}
45+
}
46+
47+
@Test(arguments: [
48+
([Double](), [Float(0.5), 0.5, 0.5, 0.5]),
49+
([Double(0.25)], [Float(0.125), 0.125, 0.125, 0.125]),
50+
([Double(0.25), 0.5], [Float(0.125), 0.25, 0.25, 0.25]),
51+
([Double(-1), 2], [Float(-0.5), 1, 1, 1]),
52+
])
53+
func opacitiesUseLevelZeroAndRepeatTheLastValue(opacities: [Double], expected: [Float]) {
54+
var shape = _ShapeStyle_Shape(operation: .resolveStyle(name: .foreground, levels: 0..<4))
55+
_OpacitiesShapeStyle(style: TestStyle(), opacities: opacities)._apply(to: &shape)
56+
57+
#expect((0..<4).map { shape.stylePack[.foreground, $0].opacity } == expected)
58+
#expect((0..<4).map { shape.stylePack[.foreground, $0].effects[0].opacity } == expected.map { $0 / 2 })
59+
guard case .resolveStyle(name: .foreground, levels: 0..<1) = shape.operation else {
60+
Issue.record("Expected the base style to resolve level zero")
61+
return
62+
}
63+
}
64+
65+
@Test
66+
func opacitiesPreserveUnrequestedLevels() {
67+
var shape = _ShapeStyle_Shape(operation: .resolveStyle(name: .foreground, levels: 2..<4))
68+
_OpacitiesShapeStyle(style: TestStyle(), opacities: [0.25, 0.5])._apply(to: &shape)
69+
70+
#expect(shape.stylePack[.foreground, 0].opacity == 0.5)
71+
#expect(shape.stylePack[.foreground, 1].opacity == 0.5)
72+
#expect(shape.stylePack[.foreground, 2].opacity == 0.25)
73+
#expect(shape.stylePack[.foreground, 3].opacity == 0.25)
74+
#expect(shape.stylePack[.background, 0].opacity == 0.5)
75+
}
76+
77+
@Test(arguments: [
78+
([Double](), -1, 1.0),
79+
([Double(0.25), 0.5], -1, 1.0),
80+
([Double(0.25), 0.5], 0, 0.25),
81+
([Double(0.25), 0.5], 1, 0.5),
82+
([Double(0.25), 0.5], 5, 0.5),
83+
])
84+
func fallbackColorUsesLevelZero(opacities: [Double], level: Int, expected: Double) {
85+
var shape = _ShapeStyle_Shape(operation: .fallbackColor(level: level))
86+
_OpacitiesShapeStyle(style: TestStyle(), opacities: opacities)._apply(to: &shape)
87+
88+
guard case let .color(color) = shape.result else {
89+
Issue.record("Expected a fallback color")
90+
return
91+
}
92+
#expect(color == Color.red.opacity(expected))
93+
guard case .fallbackColor(level: 0) = shape.operation else {
94+
Issue.record("Expected fallback color resolution at level zero")
95+
return
96+
}
97+
}
98+
99+
@Test
100+
func copiedStyleRetainsOpacity() {
101+
var shape = _ShapeStyle_Shape(operation: .copyStyle(name: .foreground))
102+
_OpacityShapeStyle(style: TestStyle(), opacity: 0.5)._apply(to: &shape)
103+
guard case let .style(copied) = shape.result else {
104+
Issue.record("Expected a copied style")
105+
return
106+
}
107+
shape.operation = .fallbackColor(level: 0)
108+
copied._apply(to: &shape)
109+
guard case let .color(color) = shape.result else {
110+
Issue.record("Expected the copied style's fallback color")
111+
return
112+
}
113+
#expect(color == Color.red.opacity(0.5))
114+
}
115+
116+
@Test
117+
func opacitiesDeclareMultipleLevels() {
118+
var shape = _ShapeStyle_Shape(operation: .multiLevel)
119+
_OpacitiesShapeStyle(style: TestStyle(), opacities: [])._apply(to: &shape)
120+
guard case .bool(true) = shape.result else {
121+
Issue.record("Expected a multilevel style even with no opacity values")
122+
return
123+
}
124+
}
125+
126+
private struct TestStyle: ShapeStyle {
127+
func _apply(to shape: inout _ShapeStyle_Shape) {
128+
switch shape.operation {
129+
case .prepareText:
130+
shape.result = .preparedText(.foregroundColor(.red))
131+
case .resolveStyle:
132+
var style = _ShapeStyle_Pack.Style(.color(.white))
133+
style.opacity = 0.5
134+
style.effects = [.init(kind: .none, opacity: 0.25, _blend: nil)]
135+
var pack = _ShapeStyle_Pack.style(style, name: .foreground)
136+
for level in 1..<4 {
137+
pack[.foreground, level] = style
138+
}
139+
pack[.background, 0] = style
140+
shape.result = .pack(pack)
141+
case let .fallbackColor(level):
142+
shape.result = .color(level == 0 ? .red : .blue)
143+
case .copyStyle:
144+
shape.result = .style(AnyShapeStyle(self))
145+
case .multiLevel:
146+
shape.result = .bool(false)
147+
case .modifyBackground, .primaryStyle:
148+
break
149+
}
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)