mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00

https://ziglang.org/download/0.14.0/release-notes.html#Calling-Convention-Enhancements-and-setAlignStack-Replaced
471 lines
13 KiB
Zig
471 lines
13 KiB
Zig
const ImguiWidget = @This();
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
|
|
const gdk = @import("gdk");
|
|
const gtk = @import("gtk");
|
|
const cimgui = @import("cimgui");
|
|
const gl = @import("opengl");
|
|
|
|
const key = @import("key.zig");
|
|
const input = @import("../../input.zig");
|
|
|
|
const log = std.log.scoped(.gtk_imgui_widget);
|
|
|
|
/// This is called every frame to populate the ImGui frame.
|
|
render_callback: ?*const fn (?*anyopaque) void = null,
|
|
render_userdata: ?*anyopaque = null,
|
|
|
|
/// Our OpenGL widget
|
|
gl_area: *gtk.GLArea,
|
|
im_context: *gtk.IMContext,
|
|
|
|
/// ImGui Context
|
|
ig_ctx: *cimgui.c.ImGuiContext,
|
|
|
|
/// Our previous instant used to calculate delta time for animations.
|
|
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) orelse return error.OutOfMemory;
|
|
errdefer cimgui.c.igDestroyContext(ig_ctx);
|
|
cimgui.c.igSetCurrentContext(ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
io.BackendPlatformName = "ghostty_gtk";
|
|
|
|
// Our OpenGL area for drawing
|
|
const gl_area = gtk.GLArea.new();
|
|
gl_area.setAutoRender(@intFromBool(true));
|
|
|
|
// The GL area has to be focusable so that it can receive events
|
|
gl_area.as(gtk.Widget).setFocusable(@intFromBool(true));
|
|
gl_area.as(gtk.Widget).setFocusOnClick(@intFromBool(true));
|
|
|
|
// Clicks
|
|
const gesture_click = gtk.GestureClick.new();
|
|
errdefer gesture_click.unref();
|
|
gesture_click.as(gtk.GestureSingle).setButton(0);
|
|
gl_area.as(gtk.Widget).addController(gesture_click.as(gtk.EventController));
|
|
|
|
// Mouse movement
|
|
const ec_motion = gtk.EventControllerMotion.new();
|
|
errdefer ec_motion.unref();
|
|
gl_area.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
|
|
|
|
// Scroll events
|
|
const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
|
|
errdefer ec_scroll.unref();
|
|
gl_area.as(gtk.Widget).addController(ec_scroll.as(gtk.EventController));
|
|
|
|
// Focus controller will tell us about focus enter/exit events
|
|
const ec_focus = gtk.EventControllerFocus.new();
|
|
errdefer ec_focus.unref();
|
|
gl_area.as(gtk.Widget).addController(ec_focus.as(gtk.EventController));
|
|
|
|
// Key event controller will tell us about raw keypress events.
|
|
const ec_key = gtk.EventControllerKey.new();
|
|
errdefer ec_key.unref();
|
|
gl_area.as(gtk.Widget).addController(ec_key.as(gtk.EventController));
|
|
errdefer gl_area.as(gtk.Widget).removeController(ec_key.as(gtk.EventController));
|
|
|
|
// The input method context that we use to translate key events into
|
|
// characters. This doesn't have an event key controller attached because
|
|
// we call it manually from our own key controller.
|
|
const im_context = gtk.IMMulticontext.new();
|
|
errdefer im_context.unref();
|
|
|
|
// Signals
|
|
_ = gtk.Widget.signals.realize.connect(
|
|
gl_area,
|
|
*ImguiWidget,
|
|
gtkRealize,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.Widget.signals.unrealize.connect(
|
|
gl_area,
|
|
*ImguiWidget,
|
|
gtkUnrealize,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.Widget.signals.destroy.connect(
|
|
gl_area,
|
|
*ImguiWidget,
|
|
gtkDestroy,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.GLArea.signals.render.connect(
|
|
gl_area,
|
|
*ImguiWidget,
|
|
gtkRender,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.GLArea.signals.resize.connect(
|
|
gl_area,
|
|
*ImguiWidget,
|
|
gtkResize,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerKey.signals.key_pressed.connect(
|
|
ec_key,
|
|
*ImguiWidget,
|
|
gtkKeyPressed,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerKey.signals.key_released.connect(
|
|
ec_key,
|
|
*ImguiWidget,
|
|
gtkKeyReleased,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerFocus.signals.enter.connect(
|
|
ec_focus,
|
|
*ImguiWidget,
|
|
gtkFocusEnter,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerFocus.signals.leave.connect(
|
|
ec_focus,
|
|
*ImguiWidget,
|
|
gtkFocusLeave,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.GestureClick.signals.pressed.connect(
|
|
gesture_click,
|
|
*ImguiWidget,
|
|
gtkMouseDown,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.GestureClick.signals.released.connect(
|
|
gesture_click,
|
|
*ImguiWidget,
|
|
gtkMouseUp,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerMotion.signals.motion.connect(
|
|
ec_motion,
|
|
*ImguiWidget,
|
|
gtkMouseMotion,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.EventControllerScroll.signals.scroll.connect(
|
|
ec_scroll,
|
|
*ImguiWidget,
|
|
gtkMouseScroll,
|
|
self,
|
|
.{},
|
|
);
|
|
_ = gtk.IMContext.signals.commit.connect(
|
|
im_context,
|
|
*ImguiWidget,
|
|
gtkInputCommit,
|
|
self,
|
|
.{},
|
|
);
|
|
|
|
self.* = .{
|
|
.gl_area = gl_area,
|
|
.im_context = im_context.as(gtk.IMContext),
|
|
.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);
|
|
}
|
|
|
|
/// This should be called anytime the underlying data for the UI changes
|
|
/// so that the UI can be refreshed.
|
|
pub fn queueRender(self: *const ImguiWidget) void {
|
|
self.gl_area.queueRender();
|
|
}
|
|
|
|
/// 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 translateMouseButton(button: c_uint) ?c_int {
|
|
return switch (button) {
|
|
1 => cimgui.c.ImGuiMouseButton_Left,
|
|
2 => cimgui.c.ImGuiMouseButton_Middle,
|
|
3 => cimgui.c.ImGuiMouseButton_Right,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
|
log.debug("imgui widget destroy", .{});
|
|
self.deinit();
|
|
}
|
|
|
|
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
|
log.debug("gl surface realized", .{});
|
|
|
|
// We need to make the context current so we can call GL functions.
|
|
area.makeCurrent();
|
|
if (area.getError()) |err| {
|
|
log.err("surface failed to realize: {s}", .{err.f_message orelse "(unknown)"});
|
|
return;
|
|
}
|
|
|
|
// realize means that our OpenGL context is ready, so we can now
|
|
// initialize the ImgUI OpenGL backend for our context.
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
|
|
}
|
|
|
|
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
|
_ = area;
|
|
log.debug("gl surface unrealized", .{});
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
cimgui.ImGui_ImplOpenGL3_Shutdown();
|
|
}
|
|
|
|
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
const scale_factor = area.as(gtk.Widget).getScaleFactor();
|
|
log.debug("gl resize width={} height={} scale={}", .{
|
|
width,
|
|
height,
|
|
scale_factor,
|
|
});
|
|
|
|
// Our display size is always unscaled. We'll do the scaling in the
|
|
// style instead. This creates crisper looking fonts.
|
|
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
|
|
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
|
|
|
|
// Setup a new style and scale it appropriately.
|
|
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
|
|
defer cimgui.c.ImGuiStyle_destroy(style);
|
|
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
|
|
const active_style = cimgui.c.igGetStyle();
|
|
active_style.* = style.*;
|
|
}
|
|
|
|
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
|
|
// Setup our frame. We render twice because some ImGui behaviors
|
|
// take multiple renders to process. I don't know how to make this
|
|
// more efficient.
|
|
for (0..2) |_| {
|
|
cimgui.ImGui_ImplOpenGL3_NewFrame();
|
|
self.newFrame() catch |err| {
|
|
log.err("failed to setup frame: {}", .{err});
|
|
return 0;
|
|
};
|
|
cimgui.c.igNewFrame();
|
|
|
|
// Build our UI
|
|
if (self.render_callback) |cb| cb(self.render_userdata);
|
|
|
|
// Render
|
|
cimgui.c.igRender();
|
|
}
|
|
|
|
// OpenGL final render
|
|
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
|
|
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
|
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
|
|
|
|
return 1;
|
|
}
|
|
|
|
fn gtkMouseMotion(
|
|
_: *gtk.EventControllerMotion,
|
|
x: f64,
|
|
y: f64,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) void {
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
|
|
cimgui.c.ImGuiIO_AddMousePosEvent(
|
|
io,
|
|
@floatCast(x * scale_factor),
|
|
@floatCast(y * scale_factor),
|
|
);
|
|
self.queueRender();
|
|
}
|
|
|
|
fn gtkMouseDown(
|
|
gesture: *gtk.GestureClick,
|
|
_: c_int,
|
|
_: f64,
|
|
_: f64,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) void {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
|
|
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
|
if (translateMouseButton(gdk_button)) |button| {
|
|
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
|
|
}
|
|
}
|
|
|
|
fn gtkMouseUp(
|
|
gesture: *gtk.GestureClick,
|
|
_: c_int,
|
|
_: f64,
|
|
_: f64,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) void {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
|
if (translateMouseButton(gdk_button)) |button| {
|
|
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
|
|
}
|
|
}
|
|
|
|
fn gtkMouseScroll(
|
|
_: *gtk.EventControllerScroll,
|
|
x: f64,
|
|
y: f64,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) c_int {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
|
io,
|
|
@floatCast(x),
|
|
@floatCast(-y),
|
|
);
|
|
|
|
return @intFromBool(true);
|
|
}
|
|
|
|
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
|
|
}
|
|
|
|
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
|
|
}
|
|
|
|
fn gtkInputCommit(
|
|
_: *gtk.IMMulticontext,
|
|
bytes: [*:0]u8,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) void {
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
|
}
|
|
|
|
fn gtkKeyPressed(
|
|
ec_key: *gtk.EventControllerKey,
|
|
keyval: c_uint,
|
|
keycode: c_uint,
|
|
gtk_mods: gdk.ModifierType,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) c_int {
|
|
return @intFromBool(self.keyEvent(
|
|
.press,
|
|
ec_key,
|
|
keyval,
|
|
keycode,
|
|
gtk_mods,
|
|
));
|
|
}
|
|
|
|
fn gtkKeyReleased(
|
|
ec_key: *gtk.EventControllerKey,
|
|
keyval: c_uint,
|
|
keycode: c_uint,
|
|
gtk_mods: gdk.ModifierType,
|
|
self: *ImguiWidget,
|
|
) callconv(.c) void {
|
|
_ = self.keyEvent(
|
|
.release,
|
|
ec_key,
|
|
keyval,
|
|
keycode,
|
|
gtk_mods,
|
|
);
|
|
}
|
|
|
|
fn keyEvent(
|
|
self: *ImguiWidget,
|
|
action: input.Action,
|
|
ec_key: *gtk.EventControllerKey,
|
|
keyval: c_uint,
|
|
keycode: c_uint,
|
|
gtk_mods: gdk.ModifierType,
|
|
) bool {
|
|
_ = keycode;
|
|
|
|
self.queueRender();
|
|
|
|
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
|
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
|
|
|
const mods = key.translateMods(gtk_mods);
|
|
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
|
|
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
|
|
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
|
|
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
|
|
|
|
// If our keyval has a key, then we send that key event
|
|
if (key.keyFromKeyval(keyval)) |inputkey| {
|
|
if (inputkey.imguiKey()) |imgui_key| {
|
|
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
|
|
}
|
|
}
|
|
|
|
// Try to process the event as text
|
|
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
|
|
_ = self.im_context.filterKeypress(event);
|
|
}
|
|
|
|
return true;
|
|
}
|