diff --git a/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb8789700..000000000 --- a/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 532cd729c..000000000 --- a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Example/Example/Assets.xcassets/Contents.json b/Example/Example/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Example/Example/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift new file mode 100644 index 000000000..6eea2f996 --- /dev/null +++ b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift @@ -0,0 +1,16 @@ +// +// NamedImageUITests.swift +// OpenSwiftUIUITests + +import SnapshotTesting +import Testing +@testable import TestingHost + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct NamedImageUITests { + @Test + func decorativeLogo() { + openSwiftUIAssertSnapshot(of: NamedImageDecorativeExample()) + } +} diff --git a/Example/OpenSwiftUIUITests/View/LabeledContent/LabelsHiddenModifierUITests.swift b/Example/OpenSwiftUIUITests/View/LabeledContent/LabelsHiddenModifierUITests.swift index a7f54a7ca..2ae51b29c 100644 --- a/Example/OpenSwiftUIUITests/View/LabeledContent/LabelsHiddenModifierUITests.swift +++ b/Example/OpenSwiftUIUITests/View/LabeledContent/LabelsHiddenModifierUITests.swift @@ -28,7 +28,9 @@ struct LabelsHiddenModifierUITests { } } #if os(iOS) || os(visionOS) - openSwiftUIAssertSnapshot(of: ContentView()) + withKnownIssue(isIntermittent: true) { + openSwiftUIAssertSnapshot(of: ContentView()) + } #else withKnownIssue("checkBox style is not supported yet") { openSwiftUIAssertSnapshot(of: ContentView()) diff --git a/Example/Shared/ContentView.swift b/Example/Shared/ContentView.swift index 3d1cdbf5a..84d11ec24 100644 --- a/Example/Shared/ContentView.swift +++ b/Example/Shared/ContentView.swift @@ -13,6 +13,6 @@ import SwiftUI struct ContentView: View { var body: some View { - FlowerView() + NamedImageDecorativeExample() } } diff --git a/Example/Shared/View/Image/AsyncImageExample.swift b/Example/Shared/View/Image/AsyncImageExample.swift index 51e519ed4..3e50bfc2c 100644 --- a/Example/Shared/View/Image/AsyncImageExample.swift +++ b/Example/Shared/View/Image/AsyncImageExample.swift @@ -1,9 +1,6 @@ // // AsyncImageExample.swift -// Example -// -// Created by Kyle on 1/18/26. -// +// Shared #if OPENSWIFTUI import OpenSwiftUI diff --git a/Example/Shared/View/Image/NamedImageExample.swift b/Example/Shared/View/Image/NamedImageExample.swift new file mode 100644 index 000000000..cd8afd935 --- /dev/null +++ b/Example/Shared/View/Image/NamedImageExample.swift @@ -0,0 +1,17 @@ +// +// NamedImageExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct NamedImageDecorativeExample: View { + var body: some View { + Image(decorative: "logo") + .resizable() + .frame(width: 100, height: 100) + } +} diff --git a/Package.resolved b/Package.resolved index 1ef6fea8d..cc0153f74 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "2064a4e298e54b6062b0ca0337ed4a44b88ff837" + "revision" : "9b0f5282d12cc93e0e19c3ff1d26ee3d74458cf9" } }, { diff --git a/Sources/OpenSwiftUICore/Data/EnvironmentKeys/EnvironmentAdditions.swift b/Sources/OpenSwiftUICore/Data/EnvironmentKeys/EnvironmentAdditions.swift index 219e8ebc5..dfc7f9b83 100644 --- a/Sources/OpenSwiftUICore/Data/EnvironmentKeys/EnvironmentAdditions.swift +++ b/Sources/OpenSwiftUICore/Data/EnvironmentKeys/EnvironmentAdditions.swift @@ -69,6 +69,32 @@ extension Image { } } +#if OPENSWIFTUI_LINK_COREUI +// MARK: - Image.TemplateRenderingMode + CUIRenderMode + +package import CoreUI + +extension Image.TemplateRenderingMode { + package init?(_ renderMode: CUIRenderMode) { + switch renderMode { + case .original: + self = .original + case .template: + self = .template + case .default: + return nil + } + } + + package var cuiRenderMode: CUIRenderMode { + switch self { + case .template: .template + case .original: .original + } + } +} +#endif + // MARK: - UserInterfaceSizeClass /// A set of values that indicate the visual size available to the view. @@ -108,11 +134,47 @@ public enum UserInterfaceSizeClass: Sendable { case regular } +#if OPENSWIFTUI_LINK_COREUI +// MARK: - UserInterfaceSizeClass + CUIUserInterfaceSizeClass + +package import CoreUI + +extension UserInterfaceSizeClass { + package init?(_ sizeClass: CUIUserInterfaceSizeClass) { + switch sizeClass { + case .any: return nil + case .compact: self = .compact + case .regular: self = .regular + } + } + + package var cUISizeClass: CUIUserInterfaceSizeClass { + switch self { + case .compact: .compact + case .regular: .regular + } + } +} +#endif + +extension UserInterfaceSizeClass? { + package var rawValue: Int8 { + #if OPENSWIFTUI_LINK_COREUI + Int8((self?.cUISizeClass ?? .any).rawValue) + #else + switch self { + case .none: 0 + case .compact: 1 + case .regular: 2 + } + #endif + } +} + // MARK: - DisplayGamut #if canImport(Darwin) && OPENSWIFTUI_LINK_COREUI import CoreUI_Private -import CoreUI #endif @_spi(Private) diff --git a/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift b/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift index 476b13e77..4958cd424 100644 --- a/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift +++ b/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift @@ -2,6 +2,6 @@ // RenderBoxShims.swift // OpenSwiftUICore -package protocol RBDisplayListContents {} // RenderBox.RBDisplayListContents +public protocol RBDisplayListContents {} // RenderBox.RBDisplayListContents class RBDisplayListInterpolator {} diff --git a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift index 7b7d5bb10..fcbc5267f 100644 --- a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift @@ -239,7 +239,7 @@ package enum NamedImage { } - // MARK: - NamedImage.BitmapKey [TBA] + // MARK: - NamedImage.BitmapKey package struct BitmapKey: Hashable { package var catalogKey: CatalogKey @@ -278,8 +278,8 @@ package enum NamedImage { self.gamut = env.displayGamut self.idiom = env.cuiAssetIdiom self.subtype = env.cuiAssetSubtype - self.horizontalSizeClass = Self.convertSizeClass(env.horizontalSizeClass) - self.verticalSizeClass = Self.convertSizeClass(env.verticalSizeClass) + self.horizontalSizeClass = env.horizontalSizeClass.rawValue + self.verticalSizeClass = env.horizontalSizeClass.rawValue } package init( @@ -308,106 +308,87 @@ package enum NamedImage { self.verticalSizeClass = verticalSizeClass } - // [TBA] - // Converts UserInterfaceSizeClass? to Int8: - // nil -> 0, .compact -> 1, .regular -> 2 - private static func convertSizeClass(_ sizeClass: UserInterfaceSizeClass?) -> Int8 { - guard let sizeClass else { - return 0 - } - switch sizeClass { - case .compact: return 1 - case .regular: return 2 - } - } - func loadBitmapInfo(location: Image.Location, idiom: Int, subtype: Int) -> BitmapInfo? { #if OPENSWIFTUI_LINK_COREUI // Resolve catalog from location - guard case .bundle(let bundle) = location, - let (catalog, _) = NamedImage.sharedCache[bundle] else { - // TODO: .system / .privateSystem asset manager support - return nil + let catalog: CUICatalog + switch location { + case .bundle(let bundle): + guard let (cat, _) = NamedImage.sharedCache[bundle] else { + return nil + } + catalog = cat + case .system: + catalog = Image.Location.systemAssetManager.catalog + case .privateSystem: + catalog = Image.Location.privateSystemAssetManager.catalog } - - // Build match types based on idiom - let matchTypes = CatalogAssetMatchType.defaultValue(idiom: idiom) - let selfCUIDirection: CUILayoutDirection = layoutDirection == .leftToRight ? .LTR : .RTL - - // Find asset via appearance-matching lookup + let matchTypes = CatalogAssetMatchType.defaultValue(idiom: idiom) let namedImage: CUINamedImage? = catalog.findAsset( key: catalogKey, matchTypes: matchTypes ) { appearanceName -> CUINamedImage? in catalog.image( - withName: self.name, - scaleFactor: self.scale, + withName: name, + scaleFactor: scale, deviceIdiom: CUIDeviceIdiom(rawValue: idiom)!, - deviceSubtype: CUISubtype(rawValue: UInt(subtype)) ?? .normal, - displayGamut: CUIDisplayGamut(rawValue: UInt(self.gamut.rawValue)) ?? .SRGB, + deviceSubtype: CUISubtype(rawValue: UInt(subtype))!, + displayGamut: CUIDisplayGamut(rawValue: UInt(gamut.rawValue))!, layoutDirection: selfCUIDirection, - sizeClassHorizontal: CUIUserInterfaceSizeClass(rawValue: Int(self.horizontalSizeClass)) ?? .any, - sizeClassVertical: CUIUserInterfaceSizeClass(rawValue: Int(self.verticalSizeClass)) ?? .any, + sizeClassHorizontal: CUIUserInterfaceSizeClass(rawValue: Int(horizontalSizeClass))!, + sizeClassVertical: CUIUserInterfaceSizeClass(rawValue: Int(verticalSizeClass))!, appearanceName: appearanceName, - locale: self.locale.identifier + locale: locale ) } - guard let namedImage else { return nil } - - // Extract image contents let contents: GraphicsImage.Contents let unrotatedPixelSize: CGSize - - // TODO: Vector image path (Semantics v3 + preservedVectorRepresentation + VectorImageLayer) - // When linked on or after v3, if namedImage.preservedVectorRepresentation is true, - // attempt to get a CUINamedVectorImage from the catalog and wrap it in a - // VectorImageLayer. Falls through to CGImage path on failure. - - // CGImage path - guard let cgImage = namedImage.image else { return nil } - - // Prevent weakly-cached catalog from being deallocated while CGImage exists - if let (cat, retain) = NamedImage.sharedCache[bundle], retain { - CGImageSetProperty( - cgImage, - "org.OpenSwiftUIProject.OpenSwiftUI.ObjectToRetain" as CFString, - Unmanaged.passUnretained(cat).toOpaque() + if isLinkedOnOrAfter(.v3), + namedImage.preservedVectorRepresentation, + let namedVectorImage = catalog.namedVectorImage( + withName: name, + scaleFactor: scale, + displayGamut: CUIDisplayGamut(rawValue: UInt(gamut.rawValue))!, + layoutDirection: selfCUIDirection, + appearanceName: namedImage.appearance, + locale: locale, + ), + namedVectorImage.appearance == namedImage.appearance, + let vectorImageLayer = VectorImageLayer( + image: namedVectorImage, + location: location, + size: namedImage.size, + ) + { + contents = .vectorLayer(vectorImageLayer) + unrotatedPixelSize = namedImage.size * namedImage.scale + } else { + guard let cgImage = namedImage.image else { return nil } + // Prevent weakly-cached catalog from being deallocated while CGImage exists + // Only applies to .bundle locations (system catalogs aren't weakly cached) + if case .bundle(let bundle) = location, + let (cat, retain) = NamedImage.sharedCache[bundle], retain { + CGImageSetProperty( + cgImage, + "org.OpenSwiftUIProject.OpenSwiftUI.ObjectToRetain" as CFString, + Unmanaged.passUnretained(cat).toOpaque() + ) + } + contents = .cgImage(cgImage) + unrotatedPixelSize = CGSize( + width: CGFloat(cgImage.width), + height: CGFloat(cgImage.height) ) } - - contents = .cgImage(cgImage) - unrotatedPixelSize = CGSize( - width: CGFloat(cgImage.width), - height: CGFloat(cgImage.height) - ) - - // Template rendering mode - let renderingMode: Image.TemplateRenderingMode? - switch namedImage.templateRenderingMode { - case .original: - renderingMode = .original - case .template: - renderingMode = .template - default: - renderingMode = nil - } - - // Orientation from EXIF value + let renderingMode = Image.TemplateRenderingMode(namedImage.templateRenderingMode) var orientation = Image.Orientation(exifValue: Int(namedImage.exifOrientation) & 0xF) ?? .up - - // RTL flipping: if image is flippable and its direction doesn't match - // the requested direction, flip by XOR-ing the orientation raw value let cuiDirection = namedImage.layoutDirection if namedImage.isFlippable, cuiDirection != .unspecified, cuiDirection != selfCUIDirection { - orientation = Image.Orientation(rawValue: orientation.rawValue ^ 1)! + orientation = orientation.mirrored } - - // Scale let imageScale = namedImage.scale - - // Resizing info from 9-slice data let resizingInfo: Image.ResizingInfo? if namedImage.hasSliceInformation { let insets = namedImage.edgeInsets @@ -422,7 +403,6 @@ package enum NamedImage { } else { resizingInfo = nil } - return BitmapInfo( contents: contents, scale: imageScale, diff --git a/Sources/OpenSwiftUICore/View/Image/VectorImageLayer.swift b/Sources/OpenSwiftUICore/View/Image/VectorImageLayer.swift index 5b9a96028..5d03bf5ce 100644 --- a/Sources/OpenSwiftUICore/View/Image/VectorImageLayer.swift +++ b/Sources/OpenSwiftUICore/View/Image/VectorImageLayer.swift @@ -3,8 +3,85 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: Empty +// Status: WIP // ID: 988D5168E40F7399F12C543D2EE9C5E9 (SwiftUICore) -// TODO -package struct VectorImageLayer: Equatable {} +public import Foundation +#if OPENSWIFTUI_LINK_COREUI +package import CoreUI +#endif +@_spiOnly public import OpenRenderBoxShims +public import OpenCoreGraphicsShims + +// MARK: - VectorImageLayer [WIP] + +package struct VectorImageLayer: Hashable { + package var contents: VectorImageContents + var location: Image.Location? + var name: String? + + package init(_ contents: VectorImageContents) { + self.contents = contents + self.location = nil + self.name = nil + } + + #if OPENSWIFTUI_LINK_COREUI + package init?(image: CUINamedVectorImage, location: Image.Location, size: CGSize) { + _openSwiftUIUnimplementedFailure() + } + #endif + + #if canImport(CoreGraphics) + package init(pdfPage: CGPDFPage, size: CGSize) { + _openSwiftUIUnimplementedFailure() + } + #endif + + package var size: CGSize { + contents.size + } + + package var displayList: any RBDisplayListContents { + contents.displayList + } + + package func image(size: CGSize, imageScale: CGFloat, prefersMask: Bool = false) -> CGImage? { + contents.image(size: size, imageScale: imageScale, prefersMask: prefersMask) + } + + package static func == (lhs: VectorImageLayer, rhs: VectorImageLayer) -> Bool { + lhs.contents === rhs.contents + } + + package func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(contents)) + } +} + +// MARK: - VectorImageContents + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +open class VectorImageContents { + package init() { + _openSwiftUIEmptyStub() + } + + open var size: CGSize { + _openSwiftUIBaseClassAbstractMethod() + } + + open var displayList: any RBDisplayListContents { + _openSwiftUIBaseClassAbstractMethod() + } + + open func image(size: CGSize, imageScale: CGFloat, prefersMask: Bool) -> CGImage? { + _openSwiftUIBaseClassAbstractMethod() + } +} + +@available(*, unavailable) +extension VectorImageContents: @unchecked Sendable {} + +// TODO: RB