From 43550c18c0bd31bf5d7d995bbf8041df2e8bdea5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 13 Aug 2025 12:21:42 -0700 Subject: [PATCH] apprt/gtk-ng: imguiwidget uses signals instead of callbacks --- src/apprt/gtk-ng/build/gresource.zig | 6 +- src/apprt/gtk-ng/class/imgui_widget.zig | 158 +++++++++---------- src/apprt/gtk-ng/class/inspector_widget.zig | 39 +++-- src/apprt/gtk-ng/ui/1.5/inspector-widget.blp | 5 +- 4 files changed, 102 insertions(+), 106 deletions(-) diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 2d2738fdb..3cd385483 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -39,6 +39,9 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 2, .name = "debug-warning" }, .{ .major = 1, .minor = 3, .name = "debug-warning" }, + .{ .major = 1, .minor = 5, .name = "imgui-widget" }, + .{ .major = 1, .minor = 5, .name = "inspector-widget" }, + .{ .major = 1, .minor = 5, .name = "inspector-window" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, @@ -48,9 +51,6 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "tab" }, .{ .major = 1, .minor = 5, .name = "window" }, .{ .major = 1, .minor = 5, .name = "command-palette" }, - .{ .major = 1, .minor = 5, .name = "imgui-widget" }, - .{ .major = 1, .minor = 5, .name = "inspector-widget" }, - .{ .major = 1, .minor = 5, .name = "inspector-window" }, }; /// CSS files in css_path diff --git a/src/apprt/gtk-ng/class/imgui_widget.zig b/src/apprt/gtk-ng/class/imgui_widget.zig index 56c5370c7..1522f2bc1 100644 --- a/src/apprt/gtk-ng/class/imgui_widget.zig +++ b/src/apprt/gtk-ng/class/imgui_widget.zig @@ -1,13 +1,13 @@ const std = @import("std"); +const assert = std.debug.assert; +const cimgui = @import("cimgui"); +const gl = @import("opengl"); const adw = @import("adw"); const gdk = @import("gdk"); const gobject = @import("gobject"); const gtk = @import("gtk"); -const cimgui = @import("cimgui"); -const gl = @import("opengl"); - const input = @import("../../../input.zig"); const gresource = @import("../build/gresource.zig"); @@ -16,10 +16,11 @@ const Common = @import("../class.zig").Common; const log = std.log.scoped(.gtk_ghostty_imgui_widget); -pub const RenderCallback = *const fn (?*anyopaque) void; -pub const RenderUserdata = *anyopaque; - /// A widget for embedding a Dear ImGui application. +/// +/// It'd be a lot cleaner to use inheritance here but zig-gobject doesn't +/// currently have a way to define virtual methods, so we have to use +/// composition and signals instead. pub const ImguiWidget = extern struct { const Self = @This(); parent_instance: Parent, @@ -34,7 +35,37 @@ pub const ImguiWidget = extern struct { pub const properties = struct {}; - pub const signals = struct {}; + pub const signals = struct { + /// Emitted when the child widget should render. During the callback, + /// the Imgui context is valid. + pub const render = struct { + pub const name = "render"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + + /// Emitted when first realized to allow the embedded ImGui application + /// to initialize itself. When this is called, the ImGui context + /// is properly set. + /// + /// This might be called multiple times, but each time it is + /// called a new Imgui context will be created. + pub const setup = struct { + pub const name = "setup"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + }; const Private = struct { /// GL area where we display the Dear ImGui application. @@ -43,19 +74,13 @@ pub const ImguiWidget = extern struct { /// GTK input method context im_context: *gtk.IMMulticontext, - /// Dear ImGui context + /// Dear ImGui context. We create a context per widget so that we can + /// have multiple active imgui views in the same application. ig_context: ?*cimgui.c.ImGuiContext = null, - /// True if the the Dear ImGui OpenGL backend was initialized. - ig_gl_backend_initialized: bool = false, - /// Our previous instant used to calculate delta time for animations. instant: ?std.time.Instant = null, - /// This is called every frame to populate the Dear ImGui frame. - render_callback: ?RenderCallback = null, - render_userdata: ?RenderUserdata = null, - pub var offset: c_int = 0; }; @@ -64,21 +89,6 @@ pub const ImguiWidget = extern struct { fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - - const priv = self.private(); - - priv.ig_context = ig_context: { - const ig_context = cimgui.c.igCreateContext(null) orelse { - log.warn("unable to initialize Dear ImGui context", .{}); - break :ig_context null; - }; - errdefer cimgui.c.igDestroyContext(ig_context); - cimgui.c.igSetCurrentContext(ig_context); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); - io.BackendPlatformName = "ghostty_gtk"; - - break :ig_context ig_context; - }; } fn dispose(self: *Self) callconv(.c) void { @@ -93,49 +103,9 @@ pub const ImguiWidget = extern struct { ); } - fn finalize(self: *Self) callconv(.c) void { - const priv = self.private(); - - // If the the Dear ImGui OpenGL backend was never initialized then we - // need to destroy the Dear ImGui context manually here. If it _was_ - // initialized cleanup will be handled when the GLArea is unrealized. - if (!priv.ig_gl_backend_initialized) { - if (priv.ig_context) |ig_context| { - cimgui.c.igDestroyContext(ig_context); - priv.ig_context = null; - } - } - - gobject.Object.virtual_methods.finalize.call( - Class.parent, - self.as(Parent), - ); - } - //--------------------------------------------------------------- // Public methods - pub fn new() *Self { - return gobject.ext.newInstance(Self, .{}); - } - - /// Use to setup the Dear ImGui application. - pub fn setup(self: *Self, callback: *const fn () void) void { - self.setCurrentContext() catch return; - callback(); - } - - /// Set the callback used to render every frame. - pub fn setRenderCallback( - self: *Self, - callback: ?RenderCallback, - userdata: ?RenderUserdata, - ) void { - const priv = self.private(); - priv.render_callback = callback; - priv.render_userdata = userdata; - } - /// This should be called anytime the underlying data for the UI changes /// so that the UI can be refreshed. pub fn queueRender(self: *ImguiWidget) void { @@ -146,7 +116,9 @@ pub const ImguiWidget = extern struct { //--------------------------------------------------------------- // Private Methods - /// Set our imgui context to be current, or return an error. + /// Set our imgui context to be current, or return an error. This must be + /// called before any Dear ImGui API calls so that they're made against + /// the proper context. fn setCurrentContext(self: *Self) error{ContextNotInitialized}!void { const priv = self.private(); const ig_context = priv.ig_context orelse { @@ -238,24 +210,40 @@ pub const ImguiWidget = extern struct { fn glAreaRealize(_: *gtk.GLArea, self: *Self) callconv(.c) void { const priv = self.private(); + assert(priv.ig_context == null); priv.gl_area.makeCurrent(); if (priv.gl_area.getError()) |err| { - log.err("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"}); + log.warn("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"}); return; } + priv.ig_context = cimgui.c.igCreateContext(null) orelse { + log.warn("unable to initialize Dear ImGui context", .{}); + return; + }; self.setCurrentContext() catch return; - // realize means that our OpenGL context is ready, so we can now + // Setup some basic config + const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + io.BackendPlatformName = "ghostty_gtk"; + + // Realize means that our OpenGL context is ready, so we can now // initialize the ImgUI OpenGL backend for our context. _ = cimgui.ImGui_ImplOpenGL3_Init(null); - priv.ig_gl_backend_initialized = true; + // Setup our app + signals.setup.impl.emit( + self, + null, + .{}, + null, + ); } /// Handle a request to unrealize the GLArea fn glAreaUnrealize(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void { + assert(self.private().ig_context != null); self.setCurrentContext() catch return; cimgui.ImGui_ImplOpenGL3_Shutdown(); } @@ -292,8 +280,12 @@ pub const ImguiWidget = extern struct { cimgui.c.igNewFrame(); // Use the callback to draw the UI. - const priv = self.private(); - if (priv.render_callback) |cb| cb(priv.render_userdata); + signals.render.impl.emit( + self, + null, + .{}, + null, + ); // Render cimgui.c.igRender(); @@ -308,17 +300,17 @@ pub const ImguiWidget = extern struct { } fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { - self.queueRender(); self.setCurrentContext() catch return; const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, true); + self.queueRender(); } fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { - self.queueRender(); self.setCurrentContext() catch return; const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, false); + self.queueRender(); } fn ecKeyPressed( @@ -461,28 +453,22 @@ pub const ImguiWidget = extern struct { class.bindTemplateCallback("unrealize", &glAreaUnrealize); class.bindTemplateCallback("resize", &glAreaResize); class.bindTemplateCallback("render", &glAreaRender); - class.bindTemplateCallback("focus_enter", &ecFocusEnter); class.bindTemplateCallback("focus_leave", &ecFocusLeave); - class.bindTemplateCallback("key_pressed", &ecKeyPressed); class.bindTemplateCallback("key_released", &ecKeyReleased); - class.bindTemplateCallback("mouse_pressed", &ecMousePressed); class.bindTemplateCallback("mouse_released", &ecMouseReleased); class.bindTemplateCallback("mouse_motion", &ecMouseMotion); - class.bindTemplateCallback("scroll", &ecMouseScroll); - class.bindTemplateCallback("im_commit", &imCommit); - // Properties - // Signals + signals.render.impl.register(.{}); + signals.setup.impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); - gobject.Object.virtual_methods.finalize.implement(class, &finalize); } pub const as = C.Class.as; diff --git a/src/apprt/gtk-ng/class/inspector_widget.zig b/src/apprt/gtk-ng/class/inspector_widget.zig index b36d6b3c0..92a512712 100644 --- a/src/apprt/gtk-ng/class/inspector_widget.zig +++ b/src/apprt/gtk-ng/class/inspector_widget.zig @@ -63,10 +63,6 @@ pub const InspectorWidget = extern struct { fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - - const priv = self.private(); - priv.imgui_widget.setup(Inspector.setup); - priv.imgui_widget.setRenderCallback(imguiRender, self); } fn dispose(self: *Self) callconv(.c) void { @@ -109,18 +105,6 @@ pub const InspectorWidget = extern struct { //--------------------------------------------------------------- // Private Methods - /// This is the callback from the embedded Dear ImGui widget that is called - /// to do the actual drawing. - fn imguiRender(ud: ?*anyopaque) void { - const self: *Self = @ptrCast(@alignCast(ud orelse return)); - const priv = self.private(); - const surface = priv.surface.get() orelse return; - defer surface.unref(); - const core_surface = surface.core() orelse return; - const inspector = core_surface.inspector orelse return; - inspector.render(); - } - //--------------------------------------------------------------- // Properties @@ -166,6 +150,25 @@ pub const InspectorWidget = extern struct { //--------------------------------------------------------------- // Signal Handlers + fn imguiRender( + _: *ImguiWidget, + self: *Self, + ) callconv(.c) void { + const priv = self.private(); + const surface = priv.surface.get() orelse return; + defer surface.unref(); + const core_surface = surface.core() orelse return; + const inspector = core_surface.inspector orelse return; + inspector.render(); + } + + fn imguiSetup( + _: *ImguiWidget, + _: *Self, + ) callconv(.c) void { + Inspector.setup(); + } + const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; @@ -192,6 +195,10 @@ pub const InspectorWidget = extern struct { // Bindings class.bindTemplateChildPrivate("imgui_widget", .{}); + // Template callbacks + class.bindTemplateCallback("imgui_render", &imguiRender); + class.bindTemplateCallback("imgui_setup", &imguiSetup); + // Properties gobject.ext.registerProperties(class, &.{ properties.surface.impl, diff --git a/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp b/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp index 0cbd45d8a..985a7ed23 100644 --- a/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp +++ b/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp @@ -10,6 +10,9 @@ template $GhosttyInspectorWidget: Adw.Bin { vexpand: true; Adw.Bin { - $GhosttyImguiWidget imgui_widget {} + $GhosttyImguiWidget imgui_widget { + render => $imgui_render(); + setup => $imgui_setup(); + } } }