mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: rendering basic imgui
This commit is contained in:
@ -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 *);
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user