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
36 changes: 34 additions & 2 deletions Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +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("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())
}
}
2 changes: 1 addition & 1 deletion Example/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ import SwiftUI

struct ContentView: View {
var body: some View {
NamedImageDecorativeExample()
NamedImageExample()
}
}
34 changes: 34 additions & 0 deletions Example/Shared/View/Image/NamedImageExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,44 @@ 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")
.resizable()
.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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
124 changes: 124 additions & 0 deletions Sources/OpenSwiftUICore/View/Image/ImageRenderingMode.swift
Original file line number Diff line number Diff line change
@@ -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),
Copy link

Choose a reason for hiding this comment

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

The condition that sets context.symbolRenderingMode = .multicolor triggers whenever the effective template mode is non-template, which also includes the renderingMode: nil (reset-to-default) case; that may unintentionally change default SF Symbol behavior compared to not calling renderingMode at all. (Also applies to the same logic in resolveNamedImage below.)

Severity: medium

Fix This in Augment

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

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
}
}
}
36 changes: 15 additions & 21 deletions Sources/OpenSwiftUICore/View/Image/NamedImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,48 +81,44 @@ 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,
deviceIdiom: cuiIdiom,
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 {
Expand All @@ -131,7 +127,6 @@ package enum NamedImage {
} else {
flipsRightToLeft = false
}

let metrics = Image.LayoutMetrics(glyph: glyph, flipsRightToLeft: flipsRightToLeft)
return VectorInfo(
glyph: glyph,
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ extension Image {
}
}

// MARK: - Image.NamedResolved [TODO]
// MARK: - Image.NamedResolved

package struct NamedResolved {
package var name: String
Expand Down
Loading