diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/OSUI_Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/OSUI_Example.xcscheme index f8c687531..f9ebc5585 100644 --- a/Example/Example.xcodeproj/xcshareddata/xcschemes/OSUI_Example.xcscheme +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/OSUI_Example.xcscheme @@ -27,8 +27,13 @@ buildConfiguration = "OpenSwiftUIDebug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + ) */ diff --git a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h index bf3f753f3..18c399afa 100644 --- a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h +++ b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h @@ -88,6 +88,15 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN #endif @end +@interface UIImage (OpenSwiftUI_SPI) +@property (nonatomic, readonly) BOOL _hasImageAsset; +@property (nonatomic, readonly, nullable) IOSurfaceRef ioSurface; +@property (nonatomic, readonly) NSDirectionalEdgeInsets contentInsets; ++ (nullable UIImage *)imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle OPENSWIFTUI_SWIFT_NAME(init(named:in:)); ++ (nullable UIImage *)_systemImageNamed:(NSString *)name OPENSWIFTUI_SWIFT_NAME(init(_systemName:)); ++ (nullable UIImage *)_systemImageNamed:(NSString *)name variableValue:(double)value withConfiguration:(nullable UIImageConfiguration *)configuration OPENSWIFTUI_SWIFT_NAME(init(_systemName:variableValue:configuration:)); +@end + @interface UITraitCollection (OpenSwiftUI_SPI) @property (nonatomic, readonly, nullable) NSObject *_environmentWrapper_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_environmentWrapper); @end diff --git a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift index 80aca27b7..db19d87ba 100644 --- a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift +++ b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift @@ -29,6 +29,15 @@ extension UITraitCollection { return resolvedTraitCollection(with: environment, wrapper: wrapper) } + @inline(__always) + package func resolvedImageAssetOnlyTraitCollection(environment: EnvironmentValues) -> UITraitCollection { + resolvedTraitCollection( + with: environment, + wrapper: unsafeBitCast(_environmentWrapper, to: EnvironmentWrapper?.self), + forImageAssetsOnly: true + ) + } + private func resolvedTraitCollection( with environment: EnvironmentValues, wrapper: EnvironmentWrapper?, diff --git a/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitAppearanceConversions.swift b/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitAppearanceConversions.swift index 427339947..0b2dfb84e 100644 --- a/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitAppearanceConversions.swift +++ b/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitAppearanceConversions.swift @@ -5,7 +5,7 @@ // Status: WIP // ID: FE0226775232C57AACFCDAD271FF7831 (SwiftUI) -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit import COpenSwiftUI diff --git a/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitColorConversions.swift b/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitColorConversions.swift index 5c3879a62..4c9a62701 100644 --- a/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitColorConversions.swift +++ b/Sources/OpenSwiftUI/Integration/Graphic/AppKit/AppKitColorConversions.swift @@ -6,7 +6,7 @@ // Status: Complete // ID: 7137BB7EE57FAC34F81DC437C151F7AB (SwiftUI) -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) public import OpenSwiftUICore public import AppKit diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift index 9af9194d5..b3547f93f 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/AnyPlatformViewHost.swift @@ -54,7 +54,7 @@ struct PlatformViewLayoutInvalidator { // FIXME: Gesture System -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) class NSViewResponder {} #elseif canImport(UIKit) class UIViewResponder {} diff --git a/Sources/OpenSwiftUI/View/Image/NSImageConversions.swift b/Sources/OpenSwiftUI/View/Image/NSImageConversions.swift new file mode 100644 index 000000000..adbb6575b --- /dev/null +++ b/Sources/OpenSwiftUI/View/Image/NSImageConversions.swift @@ -0,0 +1,70 @@ +// +// NSImageConversions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +public import AppKit +public import OpenSwiftUICore +import OpenRenderBoxShims + +// MARK: - Image + NSImage + +@available(OpenSwiftUI_v1_0, *) +@available(iOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +extension Image { + + /// Creates a OpenSwiftUI image from an AppKit image instance. + /// - Parameter nsImage: The AppKit image to wrap with a OpenSwiftUI ``Image``. + /// instance. + public init(nsImage: NSImage) { + self.init(nsImage) + } +} + +// MARK: - NSImage + ImageProvider + +extension NSImage: ImageProvider { + + package func resolve(in context: ImageResolutionContext) -> Image.Resolved { + let displayScale = context.environment.displayScale + let layer = VectorImageLayer(nsImage: self, scale: displayScale) + let isTemplate = context.environment.imageIsTemplate(renderingMode: isTemplate ? .template : nil) + var graphicsImage = GraphicsImage( + contents: .vectorLayer(layer), + scale: displayScale, + unrotatedPixelSize: size * displayScale, + orientation: .up, + isTemplate: isTemplate, + resizingInfo: nil + ) + graphicsImage.allowedDynamicRange = context.effectiveAllowedDynamicRange(for: graphicsImage) + if context.environment.shouldRedactContent { + graphicsImage.redact(in: context.environment) + } + let label = AccessibilityImageLabel(resolvedAccessibilityDescription) + return Image.Resolved( + image: graphicsImage, + decorative: false, + label: label, + basePlatformItemImage: self + ) + } + + package var resolvedAccessibilityDescription: String? { + guard let description = accessibilityDescription, !description.isEmpty else { + return _defaultAccessibilityDescription + } + return description + } + + package func resolveNamedImage(in context: ImageResolutionContext) -> Image.NamedResolved? { + nil + } +} +#endif diff --git a/Sources/OpenSwiftUI/View/Image/UIImageConversions.swift b/Sources/OpenSwiftUI/View/Image/UIImageConversions.swift new file mode 100644 index 000000000..cd75cac10 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Image/UIImageConversions.swift @@ -0,0 +1,172 @@ +// +// UIImageConversions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 47E85C485E11398B3F3140DCB9554BB7 (SwiftUI) + +#if canImport(UIKit) +public import UIKit +public import OpenSwiftUICore + +// MARK: - Image + UIImage + +@available(OpenSwiftUI_v1_0, *) +@available(macOS, unavailable) +extension Image { + + /// Creates a OpenSwiftUI image from a UIKit image instance. + /// - Parameter uiImage: The UIKit image to wrap with a OpenSwiftUI ``Image`` + /// instance. + public init(uiImage: UIImage) { + self.init(uiImage) + } +} + +// MARK: - UIImage + ImageProvider + +extension UIImage: ImageProvider { + + package func resolve(in context: ImageResolutionContext) -> Image.Resolved { + let resolvedImage: UIImage + if !isSymbolImage, _hasImageAsset, let imageAsset { + let overridden = traitCollection.resolvedImageAssetOnlyTraitCollection(environment: context.environment) + resolvedImage = imageAsset.image(with: overridden) + } else { + resolvedImage = self + } + return resolvedImage._resolve(in: context) + } + + package func resolveNamedImage(in context: ImageResolutionContext) -> Image.NamedResolved? { + nil + } + + // MARK: - Private + + private func _resolve(in context: ImageResolutionContext) -> Image.Resolved { + let orientation = Image.Orientation(imageOrientation) + let contents: GraphicsImage.Contents? + if let cgImage { + contents = .cgImage(cgImage) + } else if let ioSurface { + contents = .ioSurface(ioSurface) + } else { + contents = nil + } + + let scale = scale + var imageSize = size + var layoutMetrics: Image.LayoutMetrics? + if isSymbolImage { + let insets = contentInsets + let baselineOffset = baselineOffsetFromBottom ?? 0.0 + let capHeight = imageSize.height - (insets.top + baselineOffset) + layoutMetrics = Image.LayoutMetrics( + baselineOffset: baselineOffset, + capHeight: capHeight, + contentSize: imageSize, + alignmentOrigin: CGPoint(x: insets.leading, y: insets.top) + ) + imageSize.width -= insets.leading + insets.trailing + imageSize.height -= insets.top + insets.bottom + } + let unrotatedSize = imageSize.unapply(orientation) + let isTemplate = context.environment.imageIsTemplate(renderingMode: .init(renderingMode)) + var graphicsImage = GraphicsImage( + contents: contents, + scale: scale, + unrotatedPixelSize: unrotatedSize * scale, + orientation: orientation, + isTemplate: isTemplate, + resizingInfo: resizingInfo + ) + graphicsImage.allowedDynamicRange = context.effectiveAllowedDynamicRange(for: graphicsImage) + if context.environment.shouldRedactContent { + graphicsImage.redact(in: context.environment) + } + let label = AccessibilityImageLabel(accessibilityLabel) + var resolved = Image.Resolved( + image: graphicsImage, + decorative: false, + label: label, + basePlatformItemImage: self + ) + if let layoutMetrics { + resolved.layoutMetrics = layoutMetrics + } + return resolved + } + + private var resizingInfo: Image.ResizingInfo? { + guard capInsets != .zero else { return nil } + let mode: Image.ResizingMode = (resizingMode == .tile) ? .tile : .stretch + let edgeInsets = EdgeInsets( + top: capInsets.top, leading: capInsets.left, + bottom: capInsets.bottom, trailing: capInsets.right + ) + return Image.ResizingInfo(capInsets: edgeInsets, mode: mode) + } +} + +// MARK: - GraphicsImage + UIImage + +extension GraphicsImage { + private func image(with name: String, variableValue: Float?, at location: Image.Location) -> UIImage? { + if let variableValue { + switch location { + case .bundle(let bundle): + return UIImage(named: name, in: bundle, variableValue: Double(variableValue), configuration: nil) + case .system: + return UIImage(systemName: name, variableValue: Double(variableValue), configuration: nil) + case .privateSystem: + return UIImage(_systemName: name, variableValue: Double(variableValue), configuration: nil) + } + } else { + switch location { + case .bundle(let bundle): + return UIImage(named: name, in: bundle) + case .system: + return UIImage(systemName: name) + case .privateSystem: + return UIImage(_systemName: name) + @unknown default: + _openSwiftUIUnimplementedFailure() + } + } + } +} + +// MARK: - Image.TemplateRenderingMode + UIImage.RenderingMode + +extension Image.TemplateRenderingMode { + @inline(__always) + fileprivate init?(_ renderingMode: UIImage.RenderingMode) { + switch renderingMode { + case .alwaysOriginal: self = .original + case .alwaysTemplate: self = .template + default: return nil + } + } +} + +// MARK: - Image.Orientation + UIImage.Orientation + +extension Image.Orientation { + @inline(__always) + fileprivate init(_ uiImageOrientation: UIImage.Orientation) { + switch uiImageOrientation { + case .up: self = .up + case .down: self = .down + case .left: self = .left + case .right: self = .right + case .upMirrored: self = .upMirrored + case .downMirrored: self = .downMirrored + case .leftMirrored: self = .leftMirrored + case .rightMirrored: self = .rightMirrored + @unknown default: self = .up + } + } +} +#endif diff --git a/Sources/OpenSwiftUI/View/Image/VectorImageLayer.swift b/Sources/OpenSwiftUI/View/Image/VectorImageLayer.swift new file mode 100644 index 000000000..5b1635d19 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Image/VectorImageLayer.swift @@ -0,0 +1,73 @@ +// +// VectorImageLayer.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 53095E34581C439FFBDB89F0B27FB221 (SwiftUI) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +@_spi(ForOpenSwiftUIOnly) +public import OpenSwiftUICore +import OpenRenderBoxShims +public import AppKit + +// MARK: - VectorImageLayer + NSImage + +extension VectorImageLayer { + @inline(__always) + package init(nsImage: NSImage, scale: CGFloat) { + let contents = NSImageContents(image: nsImage, scale: scale) + self.init(contents) + } +} + +// MARK: - NSImageContents + +private final class NSImageContents: VectorImageContents { + var _image: NSImage + var _scale: CGFloat + var _displayList: (any ORBDisplayListContents)? + + init(image: NSImage, scale: CGFloat) { + _image = image + _scale = scale + super.init() + } + + override var size: CGSize { + _image.size + } + + override var displayList: any ORBDisplayListContents { + if let cached = _displayList { + return cached + } + let displayList = ORBDisplayList() + displayList.defaultColorSpace = .SRGB + let size = _image.size + var rect = CGRect(origin: .zero, size: size) + if let cgImage = _image.cgImage( + forProposedRect: &rect, + context: nil, + hints: [.ctm: AffineTransform(scale: _scale)] + ) { + let context: CGContext = displayList.beginCGContext(withAlpha: 1.0) + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + displayList.endCGContext() + } + let contents = displayList.moveContents() + _displayList = contents + return _displayList! + } + + override func image(size: CGSize, imageScale: CGFloat, prefersMask: Bool) -> CGImage? { + var rect = CGRect(origin: .zero, size: size) + return _image.cgImage( + forProposedRect: &rect, + context: nil, + hints: [.ctm: AffineTransform(scale: imageScale)] + ) + } +} +#endif diff --git a/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift b/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift deleted file mode 100644 index 0220387f7..000000000 --- a/Sources/OpenSwiftUICore/Util/RenderBoxShims.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// RenderBoxShims.swift -// OpenSwiftUICore - -public protocol ORBDisplayListContents {} // RenderBox.RBDisplayListContents - -package class ORBDisplayListInterpolator {} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Localized.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Localized.swift index 0702e5dfd..036716155 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Localized.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Localized.swift @@ -35,7 +35,9 @@ public struct LocalizedStringKey: Equatable, ExpressibleByStringInterpolation { private var arguments: [LocalizedStringKey.FormatArgument] public init(_ value: String) { - _openSwiftUIUnimplementedFailure() + self.key = value + self.arguments = [] + _openSwiftUIUnimplementedWarning() } @_semantics("openswiftui.localized_string_key.init_literal") diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 23ae3e6f6..7b6a182ae 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -9,6 +9,7 @@ package import Foundation package import OpenAttributeGraphShims public import OpenCoreGraphicsShims +package import OpenRenderBoxShims import UIFoundation_Private // MARK: - Text + View [WIP] diff --git a/Tests/COpenSwiftUITests/Shims/AppKitPrivateTests.swift b/Tests/COpenSwiftUITests/Shims/AppKitPrivateTests.swift index 0928b5e7d..d51fb5111 100644 --- a/Tests/COpenSwiftUITests/Shims/AppKitPrivateTests.swift +++ b/Tests/COpenSwiftUITests/Shims/AppKitPrivateTests.swift @@ -2,7 +2,7 @@ // AppKitPrivateTests.swift // OpenSwiftUI_SPITests -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit import COpenSwiftUI import Testing diff --git a/Tests/OpenSwiftUITests/View/Image/ImageConversionsTests.swift b/Tests/OpenSwiftUITests/View/Image/ImageConversionsTests.swift new file mode 100644 index 000000000..ffed31405 --- /dev/null +++ b/Tests/OpenSwiftUITests/View/Image/ImageConversionsTests.swift @@ -0,0 +1,49 @@ +// +// ImageConversionsTests.swift +// OpenSwiftUITests + +#if canImport(Darwin) +import Testing +@testable import OpenSwiftUI +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#elseif canImport(UIKit) +import UIKit +#endif + +@MainActor +struct ImageConversionsTests { + @Test + func initWithSystemUIImage() { + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + let platformImage = NSImage(named: NSImage.addTemplateName)! + let image = Image(nsImage: platformImage) + let box = image.provider as? ImageProviderBox + #else + let platformImage = UIImage(systemName: "star")! + let image = Image(uiImage: platformImage) + let box = image.provider as? ImageProviderBox + #endif + #expect(box != nil) + #expect(box?.base === platformImage) + } + + @Test + func initWithEmptyUIImage() { + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + let platformImage = NSImage(size: CGSize(width: 10, height: 10)) + let image = Image(nsImage: platformImage) + let box = image.provider as? ImageProviderBox + #else + let platformImage = UIImage() + let image = Image(uiImage: platformImage) + let box = image.provider as? ImageProviderBox + #endif + #expect(box != nil) + #expect(box?.base === platformImage) + } +} +#endif