From 69cc6d11dc5fe8ac84c635c02a3b7a00a42daf99 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Oct 2023 21:09:03 -0700 Subject: [PATCH] macos: MetalView, render an MTKView --- macos/Ghostty.xcodeproj/project.pbxproj | 8 +++ macos/Sources/Ghostty/Ghostty.SplitView.swift | 4 +- macos/Sources/Ghostty/InspectorView.swift | 70 +++++++++++++++++++ macos/Sources/Ghostty/SurfaceView.swift | 2 +- macos/Sources/Helpers/MetalView.swift | 33 +++++++++ 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 macos/Sources/Ghostty/InspectorView.swift create mode 100644 macos/Sources/Helpers/MetalView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index dd6823fd0..f88f47cda 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; }; A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; + A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; + A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; }; @@ -59,6 +61,8 @@ A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = ""; }; A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; + A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -115,6 +119,7 @@ children = ( A5CEAFFE29C2410700646FDA /* Backport.swift */, 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */, + A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5FECBD829D2010400022361 /* WindowAccessor.swift */, A5CEAFDA29B8005900646FDA /* SplitView */, ); @@ -151,6 +156,7 @@ A55B7BB729B6F53A0055DE60 /* Package.swift */, A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, + A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, A55B7BBD29B701360055DE60 /* Ghostty.SplitView.swift */, @@ -288,6 +294,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A53426392A7DC55C00EBB7A2 /* PrimaryWindowManager.swift in Sources */, 85DE1C922A6A3DCA00493853 /* PrimaryWindow.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, @@ -304,6 +311,7 @@ A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, + A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, A5FECBD929D2010400022361 /* WindowAccessor.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, diff --git a/macos/Sources/Ghostty/Ghostty.SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift index 6990daae9..8287bfb1a 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitView.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitView.swift @@ -30,7 +30,7 @@ extension Ghostty { // surface. We need to keep the split root around so that we don't // lose all of the surface state so this must be a ZStack. if let surfaceView = zoomedSurface { - SurfaceWrapper(surfaceView: surfaceView) + InspectableSurface(surfaceView: surfaceView) } } .focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil) @@ -343,7 +343,7 @@ extension Ghostty { let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface) let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface) - SurfaceWrapper(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty()) + InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty()) .onReceive(pub) { onNewSplit(notification: $0) } .onReceive(pubClose) { onClose(notification: $0) } .onReceive(pubFocus) { onMoveFocus(notification: $0) } diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift new file mode 100644 index 000000000..5f0b92790 --- /dev/null +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -0,0 +1,70 @@ +import Foundation +import MetalKit +import SwiftUI + +extension Ghostty { + /// InspectableSurface is a type of Surface view that allows an inspector to be attached. + struct InspectableSurface: View { + /// Same as SurfaceWrapper, see the doc comments there. + @ObservedObject var surfaceView: SurfaceView + var isSplit: Bool = false + + var body: some View { + SplitView(.vertical, left: { + SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit) + }, right: { + InspectorView() + }) + } + } + + struct InspectorView: View { + var body: some View { + MetalView() + } + } + + class Renderer: NSObject, MetalViewRenderer { + let device: MTLDevice + let commandQueue: MTLCommandQueue + + required init(metalView: MTKView) { + // Initialize our Metal primitives + guard + let device = MTLCreateSystemDefaultDevice(), + let commandQueue = device.makeCommandQueue() else { + fatalError("GPU not available") + } + + self.device = device + self.commandQueue = commandQueue + super.init() + + // Setup the view to point to this renderer + metalView.device = device + metalView.delegate = self + metalView.clearColor = MTLClearColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) + } + } +} + +extension Ghostty.Renderer: MTKViewDelegate { + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + } + + func draw(in view: MTKView) { + guard + let commandBuffer = self.commandQueue.makeCommandBuffer(), + let descriptor = view.currentRenderPassDescriptor, + let renderEncoder = + commandBuffer.makeRenderCommandEncoder( + descriptor: descriptor) else { + return + } + + renderEncoder.endEncoding() + guard let drawable = view.currentDrawable else { return } + commandBuffer.present(drawable) + commandBuffer.commit() + } +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 4fa0247e2..f2ea9b7bb 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -32,7 +32,7 @@ extension Ghostty { content(surfaceView) } } - + struct SurfaceWrapper: View { // The surface to create a view for. This must be created upstream. As long as this // remains the same, the surface that is being rendered remains the same. diff --git a/macos/Sources/Helpers/MetalView.swift b/macos/Sources/Helpers/MetalView.swift new file mode 100644 index 000000000..b0dbe10ad --- /dev/null +++ b/macos/Sources/Helpers/MetalView.swift @@ -0,0 +1,33 @@ +import SwiftUI +import MetalKit + +/// Implements the logic for a metal view used by MetalView. +protocol MetalViewRenderer: MTKViewDelegate { + init(metalView: MTKView) +} + +/// Renders an MTKView with the given renderer class. +struct MetalView: View { + @State private var metalView = MTKView() + @State private var renderer: R? + + var body: some View { + MetalViewRepresentable(metalView: $metalView) + .onAppear { renderer = R(metalView: metalView) } + } +} + +fileprivate struct MetalViewRepresentable: NSViewRepresentable { + @Binding var metalView: MTKView + + func makeNSView(context: Context) -> some NSView { + metalView + } + + func updateNSView(_ view: NSViewType, context: Context) { + updateMetalView() + } + + func updateMetalView() { + } +}