Skip to content

Commit 9108d33

Browse files
authored
Add SymbolRenderingMode API support (#720)
1 parent 2e669c9 commit 9108d33

File tree

2 files changed

+268
-1
lines changed

2 files changed

+268
-1
lines changed

Sources/OpenSwiftUICore/View/Image/Image.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ package struct ImageResolutionContext {
111111

112112
package var transaction: OptionalAttribute<Transaction>
113113

114-
// package var symbolRenderingMode: SymbolRenderingMode?
114+
package var symbolRenderingMode: SymbolRenderingMode?
115115

116116
package var allowedDynamicRange: Image.DynamicRange?
117117

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
//
2+
// SymbolRenderingMode.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
// ID: FE3FF33C1D9A704A22DF5519034B23F2 (SwiftUICore)
8+
9+
// MARK: - SymbolRenderingMode
10+
11+
/// A symbol rendering mode.
12+
@available(OpenSwiftUI_v3_0, *)
13+
public struct SymbolRenderingMode: Sendable {
14+
package enum Storage: Equatable, Sendable {
15+
case monochrome
16+
case multicolor
17+
case hierarchical
18+
case palette
19+
case preferred
20+
case hierarchicalUnlessSlashed
21+
case hierarchicalSlashBadge
22+
case paletteSlashBadge
23+
}
24+
25+
package var storage: Storage
26+
27+
package init(storage: Storage) {
28+
self.storage = storage
29+
}
30+
31+
/// A mode that renders symbols as a single layer filled with the
32+
/// foreground style.
33+
///
34+
/// For example, you can render a filled exclamation mark triangle in
35+
/// purple:
36+
///
37+
/// Image(systemName: "exclamationmark.triangle.fill")
38+
/// .symbolRenderingMode(.monochrome)
39+
/// .foregroundStyle(Color.purple)
40+
public static let monochrome = SymbolRenderingMode(storage: .monochrome)
41+
42+
/// A mode that renders symbols as multiple layers with their inherit
43+
/// styles.
44+
///
45+
/// The layers may be filled with their own inherent styles, or the
46+
/// foreground style. For example, you can render a filled exclamation
47+
/// mark triangle in its inherent colors, with yellow for the triangle and
48+
/// white for the exclamation mark:
49+
///
50+
/// Image(systemName: "exclamationmark.triangle.fill")
51+
/// .symbolRenderingMode(.multicolor)
52+
public static let multicolor = SymbolRenderingMode(storage: .multicolor)
53+
54+
/// A mode that renders symbols as multiple layers, with different opacities
55+
/// applied to the foreground style.
56+
///
57+
/// OpenSwiftUI fills the first layer with the foreground style, and the others
58+
/// the secondary, and tertiary variants of the foreground style. You can
59+
/// specify these styles explicitly using the ``View/foregroundStyle(_:_:)``
60+
/// and ``View/foregroundStyle(_:_:_:)`` modifiers. If you only specify
61+
/// a primary foreground style, OpenSwiftUI automatically derives
62+
/// the others from that style. For example, you can render a filled
63+
/// exclamation mark triangle with purple as the tint color for the
64+
/// exclamation mark, and lower opacity purple for the triangle:
65+
///
66+
/// Image(systemName: "exclamationmark.triangle.fill")
67+
/// .symbolRenderingMode(.hierarchical)
68+
/// .foregroundStyle(Color.purple)
69+
public static let hierarchical = SymbolRenderingMode(storage: .hierarchical)
70+
71+
/// A mode that renders symbols as multiple layers, with different styles
72+
/// applied to the layers.
73+
///
74+
/// In this mode OpenSwiftUI maps each successively defined layer in the image
75+
/// to the next of the primary, secondary, and tertiary variants of the
76+
/// foreground style. You can specify these styles explicitly using the
77+
/// ``View/foregroundStyle(_:_:)`` and ``View/foregroundStyle(_:_:_:)``
78+
/// modifiers. If you only specify a primary foreground style, OpenSwiftUI
79+
/// automatically derives the others from that style. For example, you can
80+
/// render a filled exclamation mark triangle with yellow as the tint color
81+
/// for the exclamation mark, and fill the triangle with cyan:
82+
///
83+
/// Image(systemName: "exclamationmark.triangle.fill")
84+
/// .symbolRenderingMode(.palette)
85+
/// .foregroundStyle(Color.yellow, Color.cyan)
86+
///
87+
/// You can also omit the symbol rendering mode, as specifying multiple
88+
/// foreground styles implies switching to palette rendering mode:
89+
///
90+
/// Image(systemName: "exclamationmark.triangle.fill")
91+
/// .foregroundStyle(Color.yellow, Color.cyan)
92+
public static let palette = SymbolRenderingMode(storage: .palette)
93+
94+
package static let preferred = SymbolRenderingMode(storage: .preferred)
95+
96+
package static let preferredIfEnabled: SymbolRenderingMode? = {
97+
_SemanticFeature_v4.isEnabled ? .preferred : nil
98+
}()
99+
}
100+
101+
@_spi(Private)
102+
@available(OpenSwiftUI_v6_0, *)
103+
extension SymbolRenderingMode {
104+
public static let hierarchicalUnlessSlashed = SymbolRenderingMode(storage: .hierarchicalUnlessSlashed)
105+
106+
public static let hierarchicalSlashBadge = SymbolRenderingMode(storage: .hierarchicalSlashBadge)
107+
108+
public static let paletteSlashBadge = SymbolRenderingMode(storage: .paletteSlashBadge)
109+
}
110+
111+
// MARK: - SymbolRenderingMode.Storage + Codable
112+
113+
extension SymbolRenderingMode.Storage: Codable {
114+
private enum CodingKeys: CodingKey {
115+
case monochrome
116+
case multicolor
117+
case hierarchical
118+
case palette
119+
case preferred
120+
case hierarchicalUnlessSlashed
121+
case hierarchicalSlashBadge
122+
case paletteSlashBadge
123+
}
124+
125+
private enum MonochromeCodingKeys: CodingKey {}
126+
private enum MulticolorCodingKeys: CodingKey {}
127+
private enum HierarchicalCodingKeys: CodingKey {}
128+
private enum PaletteCodingKeys: CodingKey {}
129+
private enum PreferredCodingKeys: CodingKey {}
130+
private enum HierarchicalUnlessSlashedCodingKeys: CodingKey {}
131+
private enum HierarchicalSlashBadgeCodingKeys: CodingKey {}
132+
private enum PaletteSlashBadgeCodingKeys: CodingKey {}
133+
134+
package func encode(to encoder: any Encoder) throws {
135+
var container = encoder.container(keyedBy: CodingKeys.self)
136+
switch self {
137+
case .monochrome:
138+
_ = container.nestedContainer(keyedBy: MonochromeCodingKeys.self, forKey: .monochrome)
139+
case .multicolor:
140+
_ = container.nestedContainer(keyedBy: MulticolorCodingKeys.self, forKey: .multicolor)
141+
case .hierarchical:
142+
_ = container.nestedContainer(keyedBy: HierarchicalCodingKeys.self, forKey: .hierarchical)
143+
case .palette:
144+
_ = container.nestedContainer(keyedBy: PaletteCodingKeys.self, forKey: .palette)
145+
case .preferred:
146+
_ = container.nestedContainer(keyedBy: PreferredCodingKeys.self, forKey: .preferred)
147+
case .hierarchicalUnlessSlashed:
148+
_ = container.nestedContainer(keyedBy: HierarchicalUnlessSlashedCodingKeys.self, forKey: .hierarchicalUnlessSlashed)
149+
case .hierarchicalSlashBadge:
150+
_ = container.nestedContainer(keyedBy: HierarchicalSlashBadgeCodingKeys.self, forKey: .hierarchicalSlashBadge)
151+
case .paletteSlashBadge:
152+
_ = container.nestedContainer(keyedBy: PaletteSlashBadgeCodingKeys.self, forKey: .paletteSlashBadge)
153+
}
154+
}
155+
156+
package init(from decoder: any Decoder) throws {
157+
let container = try decoder.container(keyedBy: CodingKeys.self)
158+
let keys = container.allKeys
159+
guard keys.count == 1 else {
160+
throw DecodingError.typeMismatch(
161+
Self.self,
162+
DecodingError.Context(
163+
codingPath: container.codingPath,
164+
debugDescription: "Invalid number of keys found, expected one."
165+
)
166+
)
167+
}
168+
switch keys[0] {
169+
case .monochrome:
170+
_ = try container.nestedContainer(keyedBy: MonochromeCodingKeys.self, forKey: .monochrome)
171+
self = .monochrome
172+
case .multicolor:
173+
_ = try container.nestedContainer(keyedBy: MulticolorCodingKeys.self, forKey: .multicolor)
174+
self = .multicolor
175+
case .hierarchical:
176+
_ = try container.nestedContainer(keyedBy: HierarchicalCodingKeys.self, forKey: .hierarchical)
177+
self = .hierarchical
178+
case .palette:
179+
_ = try container.nestedContainer(keyedBy: PaletteCodingKeys.self, forKey: .palette)
180+
self = .palette
181+
case .preferred:
182+
_ = try container.nestedContainer(keyedBy: PreferredCodingKeys.self, forKey: .preferred)
183+
self = .preferred
184+
case .hierarchicalUnlessSlashed:
185+
_ = try container.nestedContainer(keyedBy: HierarchicalUnlessSlashedCodingKeys.self, forKey: .hierarchicalUnlessSlashed)
186+
self = .hierarchicalUnlessSlashed
187+
case .hierarchicalSlashBadge:
188+
_ = try container.nestedContainer(keyedBy: HierarchicalSlashBadgeCodingKeys.self, forKey: .hierarchicalSlashBadge)
189+
self = .hierarchicalSlashBadge
190+
case .paletteSlashBadge:
191+
_ = try container.nestedContainer(keyedBy: PaletteSlashBadgeCodingKeys.self, forKey: .paletteSlashBadge)
192+
self = .paletteSlashBadge
193+
}
194+
}
195+
}
196+
197+
// MARK: - EnvironmentValues + symbolRenderingMode
198+
199+
private struct SymbolRenderingModeKey: EnvironmentKey {
200+
static let defaultValue: SymbolRenderingMode? = nil
201+
}
202+
203+
@available(OpenSwiftUI_v3_0, *)
204+
extension EnvironmentValues {
205+
206+
/// The current symbol rendering mode, or `nil` denoting that the
207+
/// mode is picked automatically using the current image and
208+
/// foreground style as parameters.
209+
public var symbolRenderingMode: SymbolRenderingMode? {
210+
get { self[SymbolRenderingModeKey.self] }
211+
set { self[SymbolRenderingModeKey.self] = newValue }
212+
}
213+
}
214+
215+
// MARK: - View + symbolRenderingMode
216+
217+
@available(OpenSwiftUI_v3_0, *)
218+
extension View {
219+
220+
/// Sets the rendering mode for symbol images within this view.
221+
///
222+
/// - Parameter mode: The symbol rendering mode to use.
223+
///
224+
/// - Returns: A view that uses the rendering mode you supply.
225+
@inlinable
226+
nonisolated public func symbolRenderingMode(_ mode: SymbolRenderingMode?) -> some View {
227+
return environment(\.symbolRenderingMode, mode)
228+
}
229+
}
230+
231+
// MARK: - Image + symbolRenderingMode
232+
233+
@available(OpenSwiftUI_v3_0, *)
234+
extension Image {
235+
236+
/// Sets the rendering mode for symbol images within this view.
237+
///
238+
/// - Parameter mode: The symbol rendering mode to use.
239+
///
240+
/// - Returns: A view that uses the rendering mode you supply.
241+
public func symbolRenderingMode(_ mode: SymbolRenderingMode?) -> Image {
242+
Image(
243+
SymbolRenderingModeProvider(
244+
base: self,
245+
mode: mode?.storage
246+
)
247+
)
248+
}
249+
250+
private struct SymbolRenderingModeProvider: ImageProvider {
251+
var base: Image
252+
253+
var mode: SymbolRenderingMode.Storage?
254+
255+
func resolve(in context: ImageResolutionContext) -> Image.Resolved {
256+
var context = context
257+
context.symbolRenderingMode = mode.map { .init(storage: $0) }
258+
return base.resolve(in: context)
259+
}
260+
261+
func resolveNamedImage(in context: ImageResolutionContext) -> Image.NamedResolved? {
262+
var context = context
263+
context.symbolRenderingMode = mode.map { .init(storage: $0) }
264+
return base.resolveNamedImage(in: context)
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)