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_app_t;
|
||||||
typedef void *ghostty_config_t;
|
typedef void *ghostty_config_t;
|
||||||
typedef void *ghostty_surface_t;
|
typedef void *ghostty_surface_t;
|
||||||
|
typedef void *ghostty_inspector_t;
|
||||||
|
|
||||||
// Enums are up top so we can reference them later.
|
// Enums are up top so we can reference them later.
|
||||||
typedef enum {
|
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);
|
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 *);
|
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.
|
// 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.
|
// Don't use these unless you know what you're doing.
|
||||||
void ghostty_set_window_background_blur(ghostty_surface_t, void *);
|
void ghostty_set_window_background_blur(ghostty_surface_t, void *);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
extension Ghostty {
|
extension Ghostty {
|
||||||
/// InspectableSurface is a type of Surface view that allows an inspector to be attached.
|
/// InspectableSurface is a type of Surface view that allows an inspector to be attached.
|
||||||
@ -13,20 +14,38 @@ extension Ghostty {
|
|||||||
SplitView(.vertical, left: {
|
SplitView(.vertical, left: {
|
||||||
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
|
SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
|
||||||
}, right: {
|
}, right: {
|
||||||
SurfaceInspector()
|
InspectorViewRepresentable(surfaceView: surfaceView)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SurfaceInspector: View {
|
struct InspectorViewRepresentable: NSViewRepresentable {
|
||||||
var body: some View {
|
/// The surface that this inspector represents.
|
||||||
MetalView<InspectorView>()
|
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 {
|
class InspectorView: MTKView {
|
||||||
let commandQueue: MTLCommandQueue
|
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?) {
|
override init(frame: CGRect, device: MTLDevice?) {
|
||||||
// Initialize our Metal primitives
|
// Initialize our Metal primitives
|
||||||
guard
|
guard
|
||||||
@ -41,7 +60,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
// After initializing the parent we can set our own properties
|
// After initializing the parent we can set our own properties
|
||||||
self.device = MTLCreateSystemDefaultDevice()
|
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
|
// Setup our tracking areas for mouse events
|
||||||
updateTrackingAreas()
|
updateTrackingAreas()
|
||||||
@ -55,6 +74,30 @@ extension Ghostty {
|
|||||||
trackingAreas.forEach { removeTrackingArea($0) }
|
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() {
|
override func updateTrackingAreas() {
|
||||||
// To update our tracking area we just recreate it all.
|
// To update our tracking area we just recreate it all.
|
||||||
trackingAreas.forEach { removeTrackingArea($0) }
|
trackingAreas.forEach { removeTrackingArea($0) }
|
||||||
@ -76,17 +119,31 @@ extension Ghostty {
|
|||||||
userInfo: nil))
|
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) {
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
guard
|
guard
|
||||||
let commandBuffer = self.commandQueue.makeCommandBuffer(),
|
let commandBuffer = self.commandQueue.makeCommandBuffer(),
|
||||||
let descriptor = self.currentRenderPassDescriptor,
|
let descriptor = self.currentRenderPassDescriptor else {
|
||||||
let renderEncoder =
|
|
||||||
commandBuffer.makeRenderCommandEncoder(
|
|
||||||
descriptor: descriptor) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ghostty_inspector_metal_render(
|
||||||
|
inspector,
|
||||||
|
Unmanaged.passRetained(commandBuffer).toOpaque(),
|
||||||
|
Unmanaged.passRetained(descriptor).toOpaque()
|
||||||
|
)
|
||||||
|
|
||||||
renderEncoder.endEncoding()
|
|
||||||
guard let drawable = self.currentDrawable else { return }
|
guard let drawable = self.currentDrawable else { return }
|
||||||
commandBuffer.present(drawable)
|
commandBuffer.present(drawable)
|
||||||
commandBuffer.commit()
|
commandBuffer.commit()
|
||||||
|
@ -233,7 +233,7 @@ extension Ghostty {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The NSView implementation for a terminal surface.
|
/// The NSView implementation for a terminal surface.
|
||||||
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
class SurfaceView: NSView, NSTextInputClient, ObservableObject {
|
||||||
// The current title of the surface as defined by the pty. This can be
|
// 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.
|
// then the view is moved to a new window.
|
||||||
var initialSize: NSSize? = nil
|
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?
|
private(set) var surface: ghostty_surface_t?
|
||||||
var error: Error? = nil
|
var error: Error? = nil
|
||||||
|
|
||||||
|
@ -195,6 +195,7 @@ pub const Surface = struct {
|
|||||||
cursor_pos: apprt.CursorPos,
|
cursor_pos: apprt.CursorPos,
|
||||||
opts: Options,
|
opts: Options,
|
||||||
keymap_state: input.Keymap.State,
|
keymap_state: input.Keymap.State,
|
||||||
|
inspector: ?*Inspector = null,
|
||||||
|
|
||||||
pub const Options = extern struct {
|
pub const Options = extern struct {
|
||||||
/// Userdata passed to some of the callbacks.
|
/// Userdata passed to some of the callbacks.
|
||||||
@ -290,6 +291,9 @@ pub const Surface = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Surface) void {
|
pub fn deinit(self: *Surface) void {
|
||||||
|
// Shut down our inspector
|
||||||
|
self.freeInspector();
|
||||||
|
|
||||||
// Remove ourselves from the list of known surfaces in the app.
|
// Remove ourselves from the list of known surfaces in the app.
|
||||||
self.app.core_app.deleteSurface(self);
|
self.app.core_app.deleteSurface(self);
|
||||||
|
|
||||||
@ -297,6 +301,28 @@ pub const Surface = struct {
|
|||||||
self.core_surface.deinit();
|
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 {
|
pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void {
|
||||||
const func = self.app.opts.new_split orelse {
|
const func = self.app.opts.new_split orelse {
|
||||||
log.info("runtime embedder does not support splits", .{});
|
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
|
// C API
|
||||||
pub const CAPI = struct {
|
pub const CAPI = struct {
|
||||||
const global = &@import("../main.zig").state;
|
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.
|
/// 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
|
/// I do this in Zig as an extern function because I don't know how to
|
||||||
/// call these functions in Swift.
|
/// call these functions in Swift.
|
||||||
|
Reference in New Issue
Block a user