diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..0165212 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MetalUI", + platforms: [ + .iOS(.v13), + .macOS(.v10_15) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "MetalUI", + targets: ["MetalUI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "MetalUI", + dependencies: []), + .testTarget( + name: "MetalUITests", + dependencies: ["MetalUI"]), + ] +) diff --git a/Sources/MetalUI/Data/MetalRenderingVertex.swift b/Sources/MetalUI/Data/MetalRenderingVertex.swift new file mode 100644 index 0000000..a3888e2 --- /dev/null +++ b/Sources/MetalUI/Data/MetalRenderingVertex.swift @@ -0,0 +1,14 @@ +import MetalKit + +public struct MetalRenderingVertex { + public var position: SIMD3 + public var color: SIMD4 + + public init( + position: SIMD3, + color: SIMD4 + ) { + self.position = position + self.color = color + } +} diff --git a/Sources/MetalUI/MTKView/MetalPresenting.swift b/Sources/MetalUI/MTKView/MetalPresenting.swift new file mode 100644 index 0000000..b322ddd --- /dev/null +++ b/Sources/MetalUI/MTKView/MetalPresenting.swift @@ -0,0 +1,26 @@ +import MetalKit + +public protocol MetalPresenting: MTKView { + var renderer: MetalRendering! { get set } + + init() + + func configure(device: MTLDevice?) + + func configureMTKView() + func renderer(forDevice device: MTLDevice) -> MetalRendering +} + +public extension MetalPresenting { + + func configure(device: MTLDevice? = MTLCreateSystemDefaultDevice()) { + // Make sure we are on a device that can run metal! + guard let defaultDevice = device else { + fatalError("Device loading error") + } + + self.renderer = renderer(forDevice: defaultDevice) + self.delegate = renderer + self.configureMTKView() + } +} diff --git a/Sources/MetalUI/MTKViewDelegate/MetalRendering.swift b/Sources/MetalUI/MTKViewDelegate/MetalRendering.swift new file mode 100644 index 0000000..9721318 --- /dev/null +++ b/Sources/MetalUI/MTKViewDelegate/MetalRendering.swift @@ -0,0 +1,38 @@ +import MetalKit + +public protocol MetalRendering: NSObject, MTKViewDelegate { + var commandQueue: MTLCommandQueue? { get set } + var renderPipelineState: MTLRenderPipelineState? { get set } + var vertexBuffer: MTLBuffer? { get set } + + var vertices: [MetalRenderingVertex] { get set } + + init() + init( + vertices: [MetalRenderingVertex], + device: MTLDevice + ) + + func createCommandQueue(device: MTLDevice) + func createPipelineState( + withLibrary library: MTLLibrary?, + forDevice device: MTLDevice + ) + func createBuffers(device: MTLDevice) + + // MARK: MTKViewDelegate + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) + func draw(in view: MTKView) +} + +public extension MetalRendering { + init(vertices: [MetalRenderingVertex], device: MTLDevice) { + self.init() + + self.vertices = vertices + + createCommandQueue(device: device) + createPipelineState(withLibrary: device.makeDefaultLibrary(), forDevice: device) + createBuffers(device: device) + } +} diff --git a/Sources/MetalUI/SwiftUI/MetalView.swift b/Sources/MetalUI/SwiftUI/MetalView.swift new file mode 100644 index 0000000..64c20cd --- /dev/null +++ b/Sources/MetalUI/SwiftUI/MetalView.swift @@ -0,0 +1,83 @@ +import SwiftUI +import MetalKit + +#if os(iOS) +public struct MetalView: UIViewRepresentable where Content: MetalPresenting { + public var wrappedView: Content + + private var handleUpdateUIView: ((Content, Context) -> Void)? + private var handleMakeUIView: ((Context) -> Content)? + + public init(closure: () -> Content) { + wrappedView = closure() + } + + public func makeUIView(context: Context) -> Content { + guard let handler = handleMakeUIView else { + return wrappedView + } + + return handler(context) + } + + public func updateUIView(_ uiView: Content, context: Context) { + handleUpdateUIView?(uiView, context) + } +} + +public extension MetalView { + mutating func setMakeUIView(handler: @escaping (Context) -> Content) -> Self { + handleMakeUIView = handler + + return self + } + + mutating func setUpdateUIView(handler: @escaping (Content, Context) -> Void) -> Self { + handleUpdateUIView = handler + + return self + } +} +#elseif os(macOS) +public struct MetalView: NSViewRepresentable where Content: MetalPresenting { + public typealias NSViewType = Content + + public var wrappedView: Content + + private var handleUpdateNSView: ((Content, Context) -> Void)? + private var handleMakeNSView: ((Context) -> Content)? + + public init(closure: () -> Content) { + wrappedView = closure() + } + + public func makeNSView(context: Context) -> Content { + guard let handler = handleMakeNSView else { + return wrappedView + } + + return handler(context) + } + + public func updateNSView(_ nsView: Content, context: Context) { + handleUpdateNSView?(nsView, context) + } +} + +public extension MetalView { + mutating func setMakeNSView(handler: @escaping (Context) -> Content) -> Self { + handleMakeNSView = handler + + return self + } + + mutating func setUpdateNSView(handler: @escaping (Content, Context) -> Void) -> Self { + handleUpdateNSView = handler + + return self + } +} + +#endif + + diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..384af03 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import MetalUITests + +var tests = [XCTestCaseEntry]() +tests += MetalUITests.allTests() +XCTMain(tests) diff --git a/Tests/MetalUITests/MetalUITests.swift b/Tests/MetalUITests/MetalUITests.swift new file mode 100644 index 0000000..8d8a50a --- /dev/null +++ b/Tests/MetalUITests/MetalUITests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import MetalUI + +final class MetalUITests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual("MetalUI().text", "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/MetalUITests/XCTestManifests.swift b/Tests/MetalUITests/XCTestManifests.swift new file mode 100644 index 0000000..1889a48 --- /dev/null +++ b/Tests/MetalUITests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(MetalUITests.allTests), + ] +} +#endif