Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

imgui-zig

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.

Use as a dependency

zig fetch --save=imgui git+https://github.com/zmscode/imgui-zig.git

For 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.

A frame

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.

What the wrapper changes

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.

Backends

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.

Vulkan

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.

Headless

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 produced

One 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.

Examples

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 120

The GLFW example expects GLFW under /opt/homebrew/opt/glfw; override with -Dglfw-prefix=/usr/local.

Build options

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

Steps

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

Layout

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

How the layers fit

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.

About

Zig-idiomatic bindings for Dear ImGui, built from pinned cimgui + ImGui sources. Optional GLFW and OpenGL 3 backends; runs headless for tests.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages