apprt/gtk-ng: imguiwidget uses signals instead of callbacks

pull/8212/head
Mitchell Hashimoto 2025-08-13 12:21:42 -07:00
parent bd7177a924
commit 43550c18c0
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 102 additions and 106 deletions

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -10,6 +10,9 @@ template $GhosttyInspectorWidget: Adw.Bin {
vexpand: true;
Adw.Bin {
$GhosttyImguiWidget imgui_widget {}
$GhosttyImguiWidget imgui_widget {
render => $imgui_render();
setup => $imgui_setup();
}
}
}