macos: rendering basic imgui

This commit is contained in:
Mitchell Hashimoto
2023-10-19 11:52:49 -07:00
parent 775a734892
commit 2c40183c3c
4 changed files with 289 additions and 11 deletions

View File

@ -27,6 +27,7 @@ extern "C" {
typedef void *ghostty_app_t;
typedef void *ghostty_config_t;
typedef void *ghostty_surface_t;
typedef void *ghostty_inspector_t;
// Enums are up top so we can reference them later.
typedef enum {
@ -399,6 +400,14 @@ void ghostty_surface_split_focus(ghostty_surface_t, ghostty_split_focus_directio
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, uintptr_t, void *);
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
void ghostty_inspector_free(ghostty_surface_t);
bool ghostty_inspector_metal_init(ghostty_inspector_t, void *);
void ghostty_inspector_metal_render(ghostty_inspector_t, void *, void *);
bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
// APIs I'd like to get rid of eventually but are still needed for now.
// Don't use these unless you know what you're doing.
void ghostty_set_window_background_blur(ghostty_surface_t, void *);

View File

@ -1,6 +1,7 @@
import Foundation
import MetalKit
import SwiftUI
import GhosttyKit
extension Ghostty {
/// InspectableSurface is a type of Surface view that allows an inspector to be attached.
@ -13,20 +14,38 @@ extension Ghostty {
SplitView(.vertical, left: {
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
}, right: {
SurfaceInspector()
InspectorViewRepresentable(surfaceView: surfaceView)
})
}
}
struct SurfaceInspector: View {
var body: some View {
MetalView<InspectorView>()
struct InspectorViewRepresentable: NSViewRepresentable {
/// The surface that this inspector represents.
let surfaceView: SurfaceView
func makeNSView(context: Context) -> InspectorView {
let view = InspectorView()
view.surfaceView = self.surfaceView
return view
}
func updateNSView(_ view: InspectorView, context: Context) {
view.surfaceView = self.surfaceView
}
}
class InspectorView: MTKView {
let commandQueue: MTLCommandQueue
var surfaceView: SurfaceView? = nil {
didSet { surfaceViewDidChange() }
}
private var inspector: ghostty_inspector_t? {
guard let surfaceView = self.surfaceView else { return nil }
return surfaceView.inspector
}
override init(frame: CGRect, device: MTLDevice?) {
// Initialize our Metal primitives
guard
@ -41,7 +60,7 @@ extension Ghostty {
// After initializing the parent we can set our own properties
self.device = MTLCreateSystemDefaultDevice()
self.clearColor = MTLClearColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
self.clearColor = MTLClearColor(red: 0x28 / 0xFF, green: 0x2C / 0xFF, blue: 0x34 / 0xFF, alpha: 1.0)
// Setup our tracking areas for mouse events
updateTrackingAreas()
@ -55,6 +74,30 @@ extension Ghostty {
trackingAreas.forEach { removeTrackingArea($0) }
}
// MARK: Internal Inspector Funcs
private func surfaceViewDidChange() {
guard let inspector = self.inspector else { return }
guard let device = self.device else { return }
let devicePtr = Unmanaged.passRetained(device).toOpaque()
ghostty_inspector_metal_init(inspector, devicePtr)
}
private func updateSize() {
guard let inspector = self.inspector else { return }
// Detect our X/Y scale factor so we can update our surface
let fbFrame = self.convertToBacking(self.frame)
let xScale = fbFrame.size.width / self.frame.size.width
let yScale = fbFrame.size.height / self.frame.size.height
ghostty_inspector_set_content_scale(inspector, xScale, yScale)
// When our scale factor changes, so does our fb size so we send that too
ghostty_inspector_set_size(inspector, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
}
// MARK: NSView
override func updateTrackingAreas() {
// To update our tracking area we just recreate it all.
trackingAreas.forEach { removeTrackingArea($0) }
@ -76,17 +119,31 @@ extension Ghostty {
userInfo: nil))
}
override func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties()
updateSize()
}
override func resize(withOldSuperviewSize oldSize: NSSize) {
super.resize(withOldSuperviewSize: oldSize)
updateSize()
}
// MARK: MTKView
override func draw(_ dirtyRect: NSRect) {
guard
let commandBuffer = self.commandQueue.makeCommandBuffer(),
let descriptor = self.currentRenderPassDescriptor,
let renderEncoder =
commandBuffer.makeRenderCommandEncoder(
descriptor: descriptor) else {
let descriptor = self.currentRenderPassDescriptor else {
return
}
ghostty_inspector_metal_render(
inspector,
Unmanaged.passRetained(commandBuffer).toOpaque(),
Unmanaged.passRetained(descriptor).toOpaque()
)
renderEncoder.endEncoding()
guard let drawable = self.currentDrawable else { return }
commandBuffer.present(drawable)
commandBuffer.commit()

View File

@ -233,7 +233,7 @@ extension Ghostty {
return config
}
}
/// The NSView implementation for a terminal surface.
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
// The current title of the surface as defined by the pty. This can be
@ -245,6 +245,13 @@ extension Ghostty {
// then the view is moved to a new window.
var initialSize: NSSize? = nil
// Returns the inspector instance for this surface, or nil if the
// surface has been closed.
var inspector: ghostty_inspector_t? {
guard let surface = self.surface else { return nil }
return ghostty_surface_inspector(surface)
}
private(set) var surface: ghostty_surface_t?
var error: Error? = nil

View File

@ -195,6 +195,7 @@ pub const Surface = struct {
cursor_pos: apprt.CursorPos,
opts: Options,
keymap_state: input.Keymap.State,
inspector: ?*Inspector = null,
pub const Options = extern struct {
/// Userdata passed to some of the callbacks.
@ -290,6 +291,9 @@ pub const Surface = struct {
}
pub fn deinit(self: *Surface) void {
// Shut down our inspector
self.freeInspector();
// Remove ourselves from the list of known surfaces in the app.
self.app.core_app.deleteSurface(self);
@ -297,6 +301,28 @@ pub const Surface = struct {
self.core_surface.deinit();
}
/// Initialize the inspector instance. A surface can only have one
/// inspector at any given time, so this will return the previous inspector
/// if it was already initialized.
pub fn initInspector(self: *Surface) !*Inspector {
if (self.inspector) |v| return v;
const alloc = self.app.core_app.alloc;
var inspector = try alloc.create(Inspector);
errdefer alloc.destroy(inspector);
inspector.* = try Inspector.init(self);
self.inspector = inspector;
return inspector;
}
pub fn freeInspector(self: *Surface) void {
if (self.inspector) |v| {
v.deinit();
self.app.core_app.alloc.destroy(v);
self.inspector = null;
}
}
pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void {
const func = self.app.opts.new_split orelse {
log.info("runtime embedder does not support splits", .{});
@ -781,6 +807,141 @@ pub const Surface = struct {
}
};
/// Inspector is the state required for the terminal inspector. A terminal
/// inspector is 1:1 with a Surface.
pub const Inspector = struct {
const cimgui = @import("cimgui");
surface: *Surface,
ig_ctx: *cimgui.c.ImGuiContext,
backend: ?Backend = null,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
const Backend = enum {
metal,
pub fn deinit(self: Backend) void {
switch (self) {
.metal => cimgui.c.ImGui_ImplMetal_Shutdown(),
}
}
};
pub fn init(surface: *Surface) !Inspector {
const ig_ctx = cimgui.c.igCreateContext(null);
errdefer cimgui.c.igDestroyContext(ig_ctx);
cimgui.c.igSetCurrentContext(ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
io.BackendPlatformName = "ghostty_embedded";
return .{
.surface = surface,
.ig_ctx = ig_ctx,
};
}
pub fn deinit(self: *Inspector) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
if (self.backend) |v| v.deinit();
cimgui.c.igDestroyContext(self.ig_ctx);
}
/// Initialize the inspector for a metal backend.
pub fn initMetal(self: *Inspector, device: objc.Object) bool {
defer device.msgSend(void, objc.sel("release"), .{});
cimgui.c.igSetCurrentContext(self.ig_ctx);
if (self.backend) |v| {
v.deinit();
self.backend = null;
}
if (!cimgui.c.ImGui_ImplMetal_Init(device.value)) {
log.warn("failed to initialize metal backend", .{});
return false;
}
self.backend = .metal;
log.debug("initialized metal backend", .{});
return true;
}
pub fn renderMetal(
self: *Inspector,
command_buffer: objc.Object,
desc: objc.Object,
) !void {
defer {
command_buffer.msgSend(void, objc.sel("release"), .{});
desc.msgSend(void, objc.sel("release"), .{});
}
assert(self.backend == .metal);
//log.debug("render", .{});
// Setup our imgui frame
{
cimgui.c.ImGui_ImplMetal_NewFrame(desc.value);
try self.newFrame();
cimgui.c.igNewFrame();
// Build our UI
var show: bool = true;
cimgui.c.igShowDemoWindow(&show);
// Render
cimgui.c.igRender();
}
// MTLRenderCommandEncoder
const encoder = command_buffer.msgSend(
objc.Object,
objc.sel("renderCommandEncoderWithDescriptor:"),
.{desc.value},
);
defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
cimgui.c.ImGui_ImplMetal_RenderDrawData(
cimgui.c.igGetDrawData(),
command_buffer.value,
encoder.value,
);
}
pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
io.DisplayFramebufferScale = .{
.x = @floatCast(x),
.y = @floatCast(y),
};
}
pub fn updateSize(self: *Inspector, width: u32, height: u32) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const x_scale: u32 = @intFromFloat(io.DisplayFramebufferScale.x);
const y_scale: u32 = @intFromFloat(io.DisplayFramebufferScale.y);
io.DisplaySize = .{
.x = @floatFromInt(@divFloor(width, x_scale)),
.y = @floatFromInt(@divFloor(height, y_scale)),
};
}
fn newFrame(self: *Inspector) !void {
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
// Determine our delta time
const now = try std.time.Instant.now();
io.DeltaTime = if (self.instant) |prev| delta: {
const since_ns = now.since(prev);
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
break :delta @max(0.00001, since_s);
} else (1 / 60);
self.instant = now;
}
};
// C API
pub const CAPI = struct {
const global = &@import("../main.zig").state;
@ -1047,6 +1208,50 @@ pub const CAPI = struct {
};
}
export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {
return ptr.initInspector() catch |err| {
log.err("error initializing inspector err={}", .{err});
return null;
};
}
export fn ghostty_inspector_free(ptr: *Surface) void {
ptr.freeInspector();
}
export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
return ptr.initMetal(objc.Object.fromId(device));
}
export fn ghostty_inspector_metal_render(
ptr: *Inspector,
command_buffer: objc.c.id,
descriptor: objc.c.id,
) void {
return ptr.renderMetal(
objc.Object.fromId(command_buffer),
objc.Object.fromId(descriptor),
) catch |err| {
log.err("error rendering inspector err={}", .{err});
return;
};
}
export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {
if (ptr.backend) |v| {
v.deinit();
ptr.backend = null;
}
}
export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {
ptr.updateSize(w, h);
}
export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {
ptr.updateContentScale(x, y);
}
/// Sets the window background blur on macOS to the desired value.
/// I do this in Zig as an extern function because I don't know how to
/// call these functions in Swift.