From 48b7469248f4aba041e1edaba176b9ea72a5d802 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 20:11:42 +0800 Subject: [PATCH 1/6] Add ImageRenderingMode API --- .../View/Image/ImageRenderingMode.swift | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 Sources/OpenSwiftUICore/View/Image/ImageRenderingMode.swift diff --git a/Sources/OpenSwiftUICore/View/Image/ImageRenderingMode.swift b/Sources/OpenSwiftUICore/View/Image/ImageRenderingMode.swift new file mode 100644 index 000000000..94cb4b678 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Image/ImageRenderingMode.swift @@ -0,0 +1,124 @@ +// +// ImageRenderingMode.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 6CBDCABF463BFE9CC4C20C83C3F5C7C1 (SwiftUICore) + +// MARK: - Image + renderingMode + +@available(OpenSwiftUI_v1_0, *) +extension Image { + + /// Indicates whether OpenSwiftUI renders an image as-is, or + /// by using a different mode. + /// + /// The ``TemplateRenderingMode`` enumeration has two cases: + /// ``TemplateRenderingMode/original`` and ``TemplateRenderingMode/template``. + /// The original mode renders pixels as they appear in the original source + /// image. Template mode renders all nontransparent pixels as the + /// foreground color, which you can use for purposes like creating image + /// masks. + /// + /// The following example shows both rendering modes, as applied to an icon + /// image of a green circle with darker green border: + /// + /// Image("dot_green") + /// .renderingMode(.original) + /// Image("dot_green") + /// .renderingMode(.template) + /// + /// ![Two identically-sized circle images. The circle on top is green + /// with a darker green border. The circle at the bottom is a solid color, + /// either white on a black background, or black on a white background, + /// depending on the system's current dark mode + /// setting.](OpenSwiftUI-Image-TemplateRenderingMode-dots.png) + /// + /// You also use `renderingMode` to produce multicolored system graphics + /// from the SF Symbols set. Use the ``TemplateRenderingMode/original`` + /// mode to apply a foreground color to all parts of the symbol except + /// those that have a distinct color in the graphic. The following + /// example shows three uses of the `person.crop.circle.badge.plus` symbol + /// to achieve different effects: + /// + /// * A default appearance with no foreground color or template rendering + /// mode specified. The symbol appears all black in light mode, and all + /// white in Dark Mode. + /// * The multicolor behavior achieved by using `original` template + /// rendering mode, along with a blue foreground color. This mode causes the + /// graphic to override the foreground color for distinctive parts of the + /// image, in this case the plus icon. + /// * A single-color template behavior achieved by using `template` + /// rendering mode with a blue foreground color. This mode applies the + /// foreground color to the entire image, regardless of the user's Appearance preferences. + /// + ///```swift + ///HStack { + /// Image(systemName: "person.crop.circle.badge.plus") + /// Image(systemName: "person.crop.circle.badge.plus") + /// .renderingMode(.original) + /// .foregroundColor(.blue) + /// Image(systemName: "person.crop.circle.badge.plus") + /// .renderingMode(.template) + /// .foregroundColor(.blue) + ///} + ///.font(.largeTitle) + ///``` + /// + /// ![A horizontal layout of three versions of the same symbol: a person + /// icon in a circle with a plus icon overlaid at the bottom left. Each + /// applies a diffent set of colors based on its rendering mode, as + /// described in the preceding + /// list.](OpenSwiftUI-Image-TemplateRenderingMode-sfsymbols.png) + /// + /// Use the SF Symbols app to find system images that offer the multicolor + /// feature. Keep in mind that some multicolor symbols use both the + /// foreground and accent colors. + /// + /// - Parameter renderingMode: The mode OpenSwiftUI uses to render images. + /// - Returns: A modified ``Image``. + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> Image { + Image( + RenderingModeProvider( + base: self, + renderingMode: renderingMode + ) + ) + } + + // MARK: - RenderingModeProvider + + private struct RenderingModeProvider: ImageProvider { + var base: Image + + var renderingMode: Image.TemplateRenderingMode? + + func resolve(in context: ImageResolutionContext) -> Image.Resolved { + var context = context + if !context.environment.imageIsTemplate(renderingMode: renderingMode), + context.symbolRenderingMode == nil { + context.symbolRenderingMode = .multicolor + } + var resolved = base.resolve(in: context) + switch resolved.image.contents { + case .vectorGlyph: + break + default: + resolved.image.maskColor = context.environment.imageIsTemplate(renderingMode: renderingMode) ? .white : nil + } + return resolved + } + + func resolveNamedImage(in context: ImageResolutionContext) -> Image.NamedResolved? { + var context = context + if !context.environment.imageIsTemplate(renderingMode: renderingMode), + context.symbolRenderingMode == nil { + context.symbolRenderingMode = .multicolor + } + guard var resolved = base.resolveNamedImage(in: context) else { return nil } + resolved.isTemplate = context.environment.imageIsTemplate(renderingMode: renderingMode) + return resolved + } + } +} From c6e26b91e644be9ffc624a58324200564b9391eb Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 20:26:40 +0800 Subject: [PATCH 2/6] Add example and test case --- .../View/Image/NamedImageUITests.swift | 10 ++++++++ Example/Shared/ContentView.swift | 6 ++++- .../Shared/View/Image/NamedImageExample.swift | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift index 6eea2f996..78412bc21 100644 --- a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift +++ b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift @@ -13,4 +13,14 @@ struct NamedImageUITests { func decorativeLogo() { openSwiftUIAssertSnapshot(of: NamedImageDecorativeExample()) } + + @Test + func renderingModeOriginal() { + openSwiftUIAssertSnapshot(of: NamedImageRenderingModeOriginalExample()) + } + + @Test + func renderingModeTemplate() { + openSwiftUIAssertSnapshot(of: NamedImageRenderingModeTemplateExample()) + } } diff --git a/Example/Shared/ContentView.swift b/Example/Shared/ContentView.swift index 84d11ec24..38d5ae328 100644 --- a/Example/Shared/ContentView.swift +++ b/Example/Shared/ContentView.swift @@ -13,6 +13,10 @@ import SwiftUI struct ContentView: View { var body: some View { - NamedImageDecorativeExample() + VStack { + NamedImageDecorativeExample() + NamedImageRenderingModeOriginalExample() + NamedImageRenderingModeTemplateExample() + } } } diff --git a/Example/Shared/View/Image/NamedImageExample.swift b/Example/Shared/View/Image/NamedImageExample.swift index cd8afd935..e21aad361 100644 --- a/Example/Shared/View/Image/NamedImageExample.swift +++ b/Example/Shared/View/Image/NamedImageExample.swift @@ -15,3 +15,27 @@ struct NamedImageDecorativeExample: View { .frame(width: 100, height: 100) } } + +struct NamedImageRenderingModeOriginalExample: View { + var body: some View { + Image(decorative: "logo") + .renderingMode(.original) + .resizable() + .frame(width: 100, height: 100) + } +} + +struct NamedImageRenderingModeTemplateExample: View { + var body: some View { + HStack(spacing: .zero) { + Image(decorative: "logo") + .renderingMode(.template) + .resizable() + Image(decorative: "logo") + .renderingMode(.template) + .resizable() + .foregroundStyle(.red) + } + .frame(width: 200, height: 100) + } +} From b309a0be1d3e20675d200db7ec778069adee29f6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 22:12:49 +0800 Subject: [PATCH 3/6] Fix styleResolverMode --- Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift | 4 ++-- Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift index 09a5c330a..3a2973efa 100644 --- a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift @@ -95,13 +95,13 @@ package struct GraphicsImage: Equatable, Sendable { package var styleResolverMode: ShapeStyle.ResolverMode { switch contents { - case .cgImage: - return .init() case let .vectorGlyph(resolvedVectorGlyph): return .init( rbSymbolStyleMask: resolvedVectorGlyph.animator.styleMask, location: resolvedVectorGlyph.location ) + case .none: + return .init() default: return .init(foregroundLevels: isTemplate ? 1 : 0) } diff --git a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift index 1fa077c0e..4ec8deccf 100644 --- a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift @@ -191,7 +191,7 @@ extension Image { } } - // MARK: - Image.NamedResolved [TODO] + // MARK: - Image.NamedResolved package struct NamedResolved { package var name: String From fb0aec0876f3a0d3d65bb4f88414273436383eff Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 22:46:10 +0800 Subject: [PATCH 4/6] Fix image render --- .../Shape/ShapeStyle/ShapeStyleRendering.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift index 54c38f842..b5c7947d8 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift @@ -227,9 +227,12 @@ package struct _ShapeStyle_RenderedShape { seed: contentSeed )) case let .image(graphicsImage): - // TODO: Blocked by ImagePaint - _ = graphicsImage - _openSwiftUIUnimplementedFailure() + var image = graphicsImage + image.maskColor = color + item.value = .content(DisplayList.Content( + .image(image), + seed: contentSeed + )) case let .alphaMask(maskItem): let offset = item.frame.origin item.frame = maskItem.frame.offsetBy(dx: offset.x, dy: offset.y) From f9634e9467a1373da97a756eb4c120e64fccc011 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 23:39:27 +0800 Subject: [PATCH 5/6] Fix matchType for loadVectorInfo --- .../View/Image/NamedImage.swift | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift index fcbc5267f..eefe14162 100644 --- a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift @@ -81,30 +81,28 @@ package enum NamedImage { fileprivate func loadVectorInfo(from catalog: CUICatalog, idiom: Int) -> VectorInfo? { #if OPENSWIFTUI_LINK_COREUI - let matchType: CatalogAssetMatchType = .cuiIdiom(idiom) let glyph: CUINamedVectorGlyph? = catalog.findAsset( key: catalogKey, - matchTypes: CollectionOfOne(matchType) + matchTypes: CollectionOfOne(.always) ) { appearanceName -> CUINamedVectorGlyph? in let cuiIdiom = CUIDeviceIdiom(rawValue: idiom)! - let cuiLayoutDir = self.layoutDirection.cuiLayoutDirection - let glyphSz = self.imageScale.glyphSize - let glyphWt = self.weight.glyphWeight + let cuiLayoutDir = layoutDirection.cuiLayoutDirection + let glyphSz = imageScale.glyphSize + let glyphWt = weight.glyphWeight guard var result = catalog.namedVectorGlyph( - withName: self.name, - scaleFactor: self.scale, + withName: name, + scaleFactor: scale, deviceIdiom: cuiIdiom, layoutDirection: cuiLayoutDir, glyphSize: glyphSz, glyphWeight: glyphWt, - glyphPointSize: self.pointSize, + glyphPointSize: pointSize, appearanceName: appearanceName, - locale: self.locale + locale: locale ) else { return nil } - - let sizeScale = self.symbolSizeScale(for: result) + let sizeScale = symbolSizeScale(for: result) if sizeScale != 1.0 { - let continuousWt = self.weight.glyphContinuousWeight + let continuousWt = weight.glyphContinuousWeight if let rescaled = catalog.namedVectorGlyph( withName: self.name, scaleFactor: self.scale, @@ -112,17 +110,15 @@ package enum NamedImage { layoutDirection: cuiLayoutDir, glyphContinuousSize: sizeScale, glyphContinuousWeight: continuousWt, - glyphPointSize: self.pointSize, + glyphPointSize: pointSize, appearanceName: appearanceName, - locale: self.locale + locale: locale ) { result = rescaled } } - return result } - guard let glyph else { return nil } let flipsRightToLeft: Bool if glyph.isFlippable, glyph.layoutDirection != .unspecified { @@ -131,7 +127,6 @@ package enum NamedImage { } else { flipsRightToLeft = false } - let metrics = Image.LayoutMetrics(glyph: glyph, flipsRightToLeft: flipsRightToLeft) return VectorInfo( glyph: glyph, @@ -507,10 +502,9 @@ package enum NamedImage { // calls loadVectorInfo and caches the result. fileprivate subscript(key: VectorKey, catalog: CUICatalog) -> VectorInfo? { #if OPENSWIFTUI_LINK_COREUI - let cached = data.vectors[key] - if let cached { - if let cachedCatalog = cached.catalog, cachedCatalog == catalog { - return cached + if let cachedInfo = data.vectors[key] { + if let cachedCatalog = cachedInfo.catalog, cachedCatalog == catalog { + return cachedInfo } } guard let info = key.loadVectorInfo(from: catalog, idiom: key.idiom) else { From 419b9c74f3f138709ddde4af4ddac275803e9aed Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 8 Mar 2026 23:45:55 +0800 Subject: [PATCH 6/6] Update test case and example --- .../View/Image/NamedImageUITests.swift | 42 ++++++++++++++----- Example/Shared/ContentView.swift | 6 +-- .../Shared/View/Image/NamedImageExample.swift | 10 +++++ 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift index 78412bc21..b4ce1d9d5 100644 --- a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift +++ b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift @@ -9,18 +9,40 @@ import Testing @MainActor @Suite(.snapshots(record: .never, diffTool: diffTool)) struct NamedImageUITests { - @Test + @Test("Test named image of logo with resizable") func decorativeLogo() { - openSwiftUIAssertSnapshot(of: NamedImageDecorativeExample()) + struct ContentView: View { + var body: some View { + Image(decorative: "logo") + .resizable() + .frame(width: 100, height: 100) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) } - @Test - func renderingModeOriginal() { - openSwiftUIAssertSnapshot(of: NamedImageRenderingModeOriginalExample()) - } - - @Test - func renderingModeTemplate() { - openSwiftUIAssertSnapshot(of: NamedImageRenderingModeTemplateExample()) + @Test("Test named image of logo with different renderingMode") + func renderingModeLogo() { + struct ContentView: View { + var body: some View { + VStack(spacing: .zero) { + Image(decorative: "logo") + .renderingMode(.original) + .resizable() + .frame(width: 100, height: 100) + HStack(spacing: .zero) { + Image(decorative: "logo") + .renderingMode(.template) + .resizable() + Image(decorative: "logo") + .renderingMode(.template) + .resizable() + .foregroundStyle(.red) + } + .frame(width: 200, height: 100) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) } } diff --git a/Example/Shared/ContentView.swift b/Example/Shared/ContentView.swift index 38d5ae328..14f5a72d9 100644 --- a/Example/Shared/ContentView.swift +++ b/Example/Shared/ContentView.swift @@ -13,10 +13,6 @@ import SwiftUI struct ContentView: View { var body: some View { - VStack { - NamedImageDecorativeExample() - NamedImageRenderingModeOriginalExample() - NamedImageRenderingModeTemplateExample() - } + NamedImageExample() } } diff --git a/Example/Shared/View/Image/NamedImageExample.swift b/Example/Shared/View/Image/NamedImageExample.swift index e21aad361..90ff2a4e0 100644 --- a/Example/Shared/View/Image/NamedImageExample.swift +++ b/Example/Shared/View/Image/NamedImageExample.swift @@ -8,6 +8,16 @@ import OpenSwiftUI import SwiftUI #endif +struct NamedImageExample: View { + var body: some View { + VStack { + NamedImageDecorativeExample() + NamedImageRenderingModeOriginalExample() + NamedImageRenderingModeTemplateExample() + } + } +} + struct NamedImageDecorativeExample: View { var body: some View { Image(decorative: "logo")