diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index fcd6918e4..9a508a699 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -42,6 +42,11 @@ pub fn build(b: *std.Build) !void { lib.addCSourceFile(.{ .file = imgui.path("imgui_widgets.cpp"), .flags = flags.items }); lib.addCSourceFile(.{ .file = imgui.path("imgui_tables.cpp"), .flags = flags.items }); + lib.addCSourceFile(.{ + .file = imgui.path("backends/imgui_impl_opengl3.cpp"), + .flags = flags.items, + }); + lib.installHeadersDirectoryOptions(.{ .source_dir = .{ .path = "vendor" }, .install_dir = .header, diff --git a/pkg/cimgui/c.zig b/pkg/cimgui/c.zig index f644a084b..42b620d4b 100644 --- a/pkg/cimgui/c.zig +++ b/pkg/cimgui/c.zig @@ -1,3 +1,13 @@ -pub usingnamespace @cImport({ +const c = @cImport({ + @cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", "1"); @cInclude("cimgui.h"); }); + +// Export all of the C API +pub usingnamespace c; + +// OpenGL +pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.C) void; +pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.C) void; +pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.C) void; +pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.C) void; diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig new file mode 100644 index 000000000..48d5babd1 --- /dev/null +++ b/src/apprt/gtk/ImguiWidget.zig @@ -0,0 +1,148 @@ +const ImguiWidget = @This(); + +const std = @import("std"); +const assert = std.debug.assert; + +const cimgui = @import("cimgui"); +const c = @import("c.zig"); +const gl = @import("../../renderer/opengl/main.zig"); + +const log = std.log.scoped(.gtk_imgui_widget); + +/// Our OpenGL widget +gl_area: *c.GtkGLArea, + +ig_ctx: *cimgui.c.ImGuiContext, + +/// Our previous instant used to calculate delta time. +instant: ?std.time.Instant = null, + +/// Initialize the widget. This must have a stable pointer for events. +pub fn init(self: *ImguiWidget) !void { + // Each widget gets its own imgui context so we can have multiple + // imgui views in the same application. + 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_gtk"; + + const gl_area = c.gtk_gl_area_new(); + + // Signals + _ = c.g_signal_connect_data(gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); + + self.* = .{ + .gl_area = @ptrCast(gl_area), + .ig_ctx = ig_ctx, + }; +} + +/// Deinitialize the widget. This should ONLY be called if the widget gl_area +/// was never added to a parent. Otherwise, cleanup automatically happens +/// when the widget is destroyed and this should NOT be called. +pub fn deinit(self: *ImguiWidget) void { + cimgui.c.igDestroyContext(self.ig_ctx); +} + +/// Initialize the frame. Expects that the context is already current. +fn newFrame(self: *ImguiWidget) !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; +} + +fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + _ = v; + log.debug("imgui widget destroy", .{}); + + const self: *ImguiWidget = @ptrCast(@alignCast(ud.?)); + self.deinit(); +} + +fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { + log.debug("gl surface realized", .{}); + + // We need to make the context current so we can call GL functions. + c.gtk_gl_area_make_current(area); + if (c.gtk_gl_area_get_error(area)) |err| { + log.err("surface failed to realize: {s}", .{err.*.message}); + return; + } + + // realize means that our OpenGL context is ready, so we can now + // initialize the ImgUI OpenGL backend for our context. + const self: *ImguiWidget = @ptrCast(@alignCast(ud.?)); + cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_ImplOpenGL3_Init(null); +} + +fn gtkUnrealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { + _ = area; + log.debug("gl surface unrealized", .{}); + + const self: *ImguiWidget = @ptrCast(@alignCast(ud.?)); + cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_ImplOpenGL3_Shutdown(); +} + +fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) callconv(.C) void { + const self: *ImguiWidget = @ptrCast(@alignCast(ud.?)); + cimgui.c.igSetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const scale_factor = c.gtk_widget_get_scale_factor(@ptrCast(area)); + log.debug("gl resize width={} height={} scale={}", .{ + width, + height, + scale_factor, + }); + + io.DisplaySize = .{ + .x = @floatFromInt(@divFloor(width, scale_factor)), + .y = @floatFromInt(@divFloor(height, scale_factor)), + }; + io.DisplayFramebufferScale = .{ + .x = @floatFromInt(scale_factor), + .y = @floatFromInt(scale_factor), + }; +} + +fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { + _ = area; + _ = ctx; + const self: *ImguiWidget = @ptrCast(@alignCast(ud.?)); + + // Setup our frame + cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_ImplOpenGL3_NewFrame(); + self.newFrame() catch |err| { + log.err("failed to setup frame: {}", .{err}); + return 0; + }; + cimgui.c.igNewFrame(); + + // Build our UI + var show: bool = true; + cimgui.c.igShowDemoWindow(&show); + + // Render + cimgui.c.igRender(); + + // OpenGL final render + gl.clearColor(0.45, 0.55, 0.60, 1.00); + gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + cimgui.c.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData()); + + return 1; +} diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig index 38323d618..b45ec8467 100644 --- a/src/apprt/gtk/inspector.zig +++ b/src/apprt/gtk/inspector.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const App = @import("App.zig"); const TerminalWindow = @import("Window.zig"); +const ImguiWidget = @import("ImguiWidget.zig"); const c = @import("c.zig"); const icon = @import("icon.zig"); @@ -10,14 +11,10 @@ const log = std.log.scoped(.inspector); /// A window to hold a dedicated inspector instance. pub const Window = struct { - /// Our app app: *App, - - /// Our window window: *c.GtkWindow, - - /// The window icon icon: icon.Icon, + imgui_widget: ImguiWidget, pub fn create(alloc: Allocator, app: *App) !*Window { var window = try alloc.create(Window); @@ -32,6 +29,7 @@ pub const Window = struct { .app = app, .icon = undefined, .window = undefined, + .imgui_widget = undefined, }; // Create the window @@ -44,10 +42,15 @@ pub const Window = struct { self.icon = try icon.appIcon(self.app, window); c.gtk_window_set_icon_name(gtk_window, self.icon.name); + // Initialize our imgui widget + try self.imgui_widget.init(); + errdefer self.imgui_widget.deinit(); + // Signals _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); // Show the window + c.gtk_window_set_child(gtk_window, @ptrCast(self.imgui_widget.gl_area)); c.gtk_widget_show(window); }