Zig bindings for Dear ImGui. The package:
- builds Dear ImGui and cimgui from pinned sources, so there is nothing to install and nothing vendored beyond a small shim;
- provides an idiomatic, resource-safe Zig API and the complete raw C ABI;
- optionally builds the GLFW, OpenGL 3, and Vulkan backends, wired in the correct order;
- runs headless, so ImGui frames can be built and asserted on in tests and CI.
Normal builds need no network access after the first fetch and no system ImGui.
AI coding agents can use the repository's installable imgui-zig skill for concise,
version-specific integration guidance.
zig fetch --save=imgui git+https://github.com/zmscode/imgui-zig.gitFor local development the equivalent path dependency is:
.dependencies = .{
.imgui = .{ .path = "../imgui-zig" },
},Then expose the module to your executable in build.zig:
const imgui_dependency = b.dependency("imgui", .{
.target = target,
.optimize = optimize,
.glfw = true, // optional: GLFW platform backend
.opengl3 = true, // optional: OpenGL 3 renderer backend
.vulkan = true, // optional: Vulkan renderer backend
});
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "imgui", .module = imgui_dependency.module("imgui") },
},
}),
});Declaring the import when the module is created keeps every dependency in one place. For a module
that already exists, exe.root_module.addImport("imgui", imgui_dependency.module("imgui")); does
the same thing.
-Dopengl3 requires -Dglfw; the two renderer backends are alternatives, so enable only the one
you render with.
const imgui = @import("imgui");
var ui = try imgui.Context.init(.{});
defer ui.deinit();
ui.beginFrame();
{
const visible = ui.begin("Settings", null, .{});
defer ui.end(); // required even when `visible` is false
if (visible) {
ui.text("Dear ImGui, called from Zig.");
ui.separator();
if (ui.button("Apply")) applySettings();
_ = ui.checkbox("Verbose", &verbose);
_ = ui.sliderFloat("Threshold", &threshold, 0, 1);
ui.textf("{d} items at {d:.2}", .{ count, threshold });
}
}
ui.endFrame();end is called unconditionally because that is ImGui's contract for Begin, not an oversight.
beginMenu, treeNode and the other begin* calls are the opposite: their end* runs only
when they return true. Each wrapper's doc comment says which it is.
| Dear ImGui | imgui-zig |
|---|---|
ImGui::CreateContext() / DestroyContext() |
Context.init / deinit, with defer |
| null returns and status codes | Error set (ContextCreationFailed, ...) |
ImGuiWindowFlags_NoResize | ... |
WindowFlags{ .no_resize = true } — a packed struct |
loose ImGuiCond_ integers |
Cond enum |
ImGui::Text("%d", n) C varargs |
ui.textf("{d}", .{n}), checked at compile time |
const char * + length |
slices; labels stay [:0]const u8 because ImGui stores them |
| implicit current-context | every method sets it, so contexts can coexist |
ImVec2 / ImVec4 |
Vec2 / Vec4 / Color, with Color.hex(0xRRGGBBAA) |
Anything not yet wrapped is reachable through imgui.raw, the complete translated cimgui API.
try imgui.backends.glfw_opengl3.init(window, imgui.backends.opengl3.default_glsl_version);
defer imgui.backends.glfw_opengl3.shutdown();
// each frame, before ui.beginFrame():
imgui.backends.glfw_opengl3.newFrame();
// after ui.endFrame():
imgui.backends.glfw_opengl3.render();The combined glfw_opengl3 module exists because the two backends must start platform-first and
stop renderer-first; doing it by hand in the wrong order leaves ImGui holding freed GLFW callbacks.
window is a *anyopaque — imgui-zig needs a window pointer, not a window library, so it has no
GLFW dependency of its own. The example declares the dozen GLFW entry points it uses directly.
Build with -Dvulkan. The backend is compiled with IMGUI_IMPL_VULKAN_NO_PROTOTYPES, so it
resolves every Vulkan entry point through a loader callback rather than linking libvulkan. That
is what lets it sit on top of a dynamic loader such as vk-zig
instead of fighting it — and it means loadFunctions must be called before init.
const vk = imgui.backends.vulkan;
// 1. point ImGui at your loader
fn loadVulkan(name: [*c]const u8, user_data: ?*anyopaque) callconv(.c) imgui.raw.PFN_vkVoidFunction {
const loader: *MyLoader = @ptrCast(@alignCast(user_data.?));
return loader.getInstanceProcAddr(instance, std.mem.span(name));
}
try vk.loadFunctions(api_version, loadVulkan, &my_loader);
// 2. hand it the handles you already own
try vk.init(.{
.api_version = api_version,
.instance = instance,
.physical_device = physical_device,
.device = device,
.queue_family = graphics_family,
.queue = graphics_queue,
.descriptor_pool = descriptor_pool,
.min_image_count = 2,
.image_count = swapchain_image_count,
.render_pass = render_pass,
});
defer vk.shutdown();
// 3. each frame, inside your own render pass
vk.newFrame();
ui.beginFrame();
// ... UI ...
ui.endFrame();
vk.render(command_buffer);Every handle stays owned by the caller; ImGui borrows them and must be shut down before any of
them are destroyed. Call vk.setMinImageCount after recreating the swapchain with a different
image count.
Vulkan headers come from a pinned vulkan_headers dependency, fetched lazily only when -Dvulkan
is set, so a consumer needs no system SDK. The pin matches rgfw-zig's (v1.4.352). vk-zig vendors
v1.4.356; every type ImGui's backend touches — the handles, VkPipelineRenderingCreateInfo,
VkAllocationCallbacks — is byte-identical between the two, so the trio composes.
glfw_vulkan pairs GLFW's input-only backend with the Vulkan renderer, in the right order.
A context can run with no window and no GPU, which makes ImGui layout testable:
var ui = try imgui.Context.init(.{ .headless = true, .display_size = .{ .x = 800, .y = 600 } });
defer ui.deinit();
ui.beginFrame();
// ... build the UI ...
ui.endFrame();
const totals = ui.drawTotals(); // vertices and indices actually producedOne caveat worth knowing: a newly created window draws nothing on its first frame, because
ImGui hides it while it measures its content. A test that asserts on drawTotals must run the
frame loop at least twice. tests/smoke.zig pins that behaviour in both directions.
zig build run-headless # no window, prints geometry counts
zig build run-glfw_opengl3 -Dglfw -Dopengl3 # a real window
zig build run-glfw_opengl3 -Dglfw -Dopengl3 -- --frames 120The GLFW example expects GLFW under /opt/homebrew/opt/glfw; override with
-Dglfw-prefix=/usr/local.
| Option | Default | Effect |
|---|---|---|
-Dglfw |
false |
Build the GLFW platform backend |
-Dopengl3 |
false |
Build the OpenGL 3 renderer backend (requires -Dglfw) |
-Dvulkan |
false |
Build the Vulkan renderer backend |
-Dglfw-prefix |
/opt/homebrew/opt/glfw |
Where to find GLFW |
-Ddemo |
true |
Expose showDemoWindow |
zig build # build the library and enabled examples
zig build test # run the binding tests
zig build examples # build every enabled example
zig build bindings # write the translated C bindings to zig-out/bindings| Path | What it is |
|---|---|
src/imgui.zig |
The idiomatic API |
src/backends.zig |
GLFW, OpenGL 3, and Vulkan backends, compiled in on demand |
vendor/imgui_translate.h |
Umbrella header; the single place compile-time options are set |
vendor/imgui_zig.h/.cpp |
Hand-written shim for what cimgui cannot express |
examples/ |
Headless and windowed examples |
tests/smoke.zig |
Binding tests, including flag-bit checks against the C constants |
your code
└── imgui.zig idiomatic: handles, error sets, packed-struct flags
├── imgui_raw translate-c over cimgui + the shim (imgui.raw)
│ └── cimgui generated extern "C" over the whole ImGui API
└── imgui_zig.cpp ImGuiIO accessors and the headless texture path
└── Dear ImGui (C++)
Dear ImGui is C++, so there is nothing for Zig to bind to directly. cimgui supplies the flat C
surface; the shim covers the handful of things a generated wrapper cannot reach, such as
ImGui::GetIO().Framerate and satisfying the 1.92 texture requests that let a context run
without a renderer.