diff --git a/include/ghostty.h b/include/ghostty.h index 367f14207..796f9200b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -53,6 +53,7 @@ int ghostty_app_tick(ghostty_app_t); ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); +void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); #ifdef __cplusplus } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift index 41505c5a3..2e7343441 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurfaceView.swift @@ -10,17 +10,17 @@ import GhosttyKit /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. struct TerminalSurfaceView: NSViewRepresentable { - @StateObject private var state: TerminalSurfaceState + @StateObject private var state: TerminalSurfaceView_Real init(app: ghostty_app_t) { - self._state = StateObject(wrappedValue: TerminalSurfaceState(app)) + self._state = StateObject(wrappedValue: TerminalSurfaceView_Real(app)) } func makeNSView(context: Context) -> TerminalSurfaceView_Real { // We need the view as part of the state to be created previously because // the view is sent to the Ghostty API so that it can manipulate it // directly since we draw on a render thread. - return state.view; + return state; } func updateNSView(_ view: TerminalSurfaceView_Real, context: Context) { @@ -28,22 +28,23 @@ struct TerminalSurfaceView: NSViewRepresentable { } } -/// The state for the terminal surface view. -class TerminalSurfaceState: ObservableObject { - static let logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: String(describing: TerminalSurfaceState.self) - ) +// The actual NSView implementation for the terminal surface. +class TerminalSurfaceView_Real: NSView, ObservableObject { + // We need to support being a first responder so that we can get input events + override var acceptsFirstResponder: Bool { return true } - var view: TerminalSurfaceView_Real private var surface: ghostty_surface_t? = nil private var error: Error? = nil init(_ app: ghostty_app_t) { - view = TerminalSurfaceView_Real() + // Initialize with some default frame size. The important thing is that this + // is non-zero so that our layer bounds are non-zero so that our renderer + // can do SOMETHING. + super.init(frame: NSMakeRect(0, 0, 800, 600)) + // Setup our surface. This will also initialize all the terminal IO. var surface_cfg = ghostty_surface_config_s( - nsview: Unmanaged.passUnretained(view).toOpaque(), + nsview: Unmanaged.passUnretained(self).toOpaque(), scale_factor: 2.0) guard let surface = ghostty_surface_new(app, &surface_cfg) else { self.error = AppError.surfaceCreateError @@ -53,17 +54,16 @@ class TerminalSurfaceState: ObservableObject { self.surface = surface; } - deinit { - if let surface = self.surface { - ghostty_surface_free(surface) - } + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported for this view") + } + + override func resize(withOldSuperviewSize oldSize: NSSize) { + print("LAYER: \(self.layer?.bounds)") + super.resize(withOldSuperviewSize: oldSize) + print("RESIZE: \(oldSize) NEW: \(self.bounds)") + print("LAYER: \(self.layer?.bounds)") } -} - -// The actual NSView implementation for the terminal surface. -class TerminalSurfaceView_Real: NSView { - // We need to support being a first responder so that we can get input events - override var acceptsFirstResponder: Bool { return true } override func draw(_ dirtyRect: NSRect) { print("DRAW: \(dirtyRect)") diff --git a/src/App.zig b/src/App.zig index 1d4b833f8..982deb6b0 100644 --- a/src/App.zig +++ b/src/App.zig @@ -396,4 +396,10 @@ pub const CAPI = struct { export fn ghostty_surface_free(ptr: ?*Window) void { if (ptr) |v| v.app.closeWindow(v); } + + /// Update the size of a surface. This will trigger resize notifications + /// to the pty and the renderer. + export fn ghostty_surface_set_size(win: *Window, w: u32, h: u32) void { + win.window.updateSize(w, h); + } }; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 38e5a1193..c0dc8dba8 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -13,6 +13,8 @@ const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); const CoreWindow = @import("../Window.zig"); +const log = std.log.scoped(.embedded_window); + pub const App = struct { /// Because we only expect the embedding API to be used in embedded /// environments, the options are extern so that we can expose it @@ -48,6 +50,7 @@ pub const App = struct { pub const Window = struct { nsview: objc.Object, scale_factor: f64, + core_win: *CoreWindow, pub const Options = extern struct { /// The pointer to the backing NSView for the surface. @@ -59,9 +62,9 @@ pub const Window = struct { pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { _ = app; - _ = core_win; return .{ + .core_win = core_win, .nsview = objc.Object.fromId(opts.nsview), .scale_factor = opts.scale_factor, }; @@ -113,4 +116,17 @@ pub const Window = struct { _ = self; return false; } + + pub fn updateSize(self: *const Window, width: u32, height: u32) void { + const size: apprt.WindowSize = .{ + .width = width, + .height = height, + }; + + // Call the primary callback. + self.core_win.sizeCallback(size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index c57dd7c99..77c269e48 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -568,9 +568,14 @@ pub fn render( .{@as(c_ulong, 0)}, ); + // Texture is a property of CAMetalDrawable but if you run + // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable + // which ironically doesn't implement CAMetalDrawable as a + // property so we just send a message. + const texture = surface.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @enumToInt(MTLLoadAction.clear)); attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store)); - attachment.setProperty("texture", surface.getProperty(objc.c.id, "texture").?); + attachment.setProperty("texture", texture); attachment.setProperty("clearColor", MTLClearColor{ .red = @intToFloat(f32, critical.bg.r) / 255, .green = @intToFloat(f32, critical.bg.g) / 255, @@ -673,18 +678,20 @@ fn drawCells( .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) }, ); - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @enumToInt(MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @enumToInt(MTLIndexType.uint16), - self.buf_instance.value, - @as(c_ulong, 0), - @as(c_ulong, cells.items.len), - }, - ); + if (cells.items.len > 0) { + encoder.msgSend( + void, + objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + .{ + @enumToInt(MTLPrimitiveType.triangle), + @as(c_ulong, 6), + @enumToInt(MTLIndexType.uint16), + self.buf_instance.value, + @as(c_ulong, 0), + @as(c_ulong, cells.items.len), + }, + ); + } } /// Resize the screen.