Skip to content

Instantly share code, notes, and snippets.

@Lightnet
Created June 28, 2026 07:04
Show Gist options
  • Select an option

  • Save Lightnet/9a377b86ef835c6a7b0b418b4f2ebf91 to your computer and use it in GitHub Desktop.

Select an option

Save Lightnet/9a377b86ef835c6a7b0b418b4f2ebf91 to your computer and use it in GitHub Desktop.

Information:

Zig 0.16.0

Sample test. Note the font need to load to render text.

Note it still use c code and zig a bit.

assets:
- JetBrainsMonoNL-Light.ttf
src
- main.zig
- nuklear_impl.c
- nuklear.h

link:

const std = @import("std");
// Define a simple structure for your sample configuration
const Sample = struct {
name: []const u8, // Base name (e.g., "flecs")
source: []const u8, // File path (e.g., "src/sample_flecs.zig")
};
// Manage your active samples here.
// Simply comment out or delete a line to remove it from the build.
const sample_list = [_]Sample{
// .{ .name = "flecs", .source = "tests/sample_flecs.zig" },
//.{ .name = "image", .source = "tests/sample_image.zig" },
// .{ .name = "loading", .source = "tests/sample_loading.zig" },
// .{ .name = "blank", .source = "tests/sample_blank.zig" },
// .{ .name = "drawline", .source = "tests/sample_drawline.zig" },
// .{ .name = "imagesvg", .source = "tests/images/sample_image_svg.zig" },
.{ .name = "nk", .source = "tests/sample_nk.zig" },
//.{ .name = "loading02", .source = "tests/sample_loading02.zig" },
//.{ .name = "loading03", .source = "tests/sample_loading03.zig" },
//.{ .name = "loading05", .source = "tests/sample_loading05.zig" },
//.{ .name = "fontm", .source = "tests/sample_font_module.zig" },
//.{ .name = "drag", .source = "tests/sample_drag.zig" },
// .{ .name = "future", .source = "src/sample_future.zig" },
//
};
pub fn build(b: *std.Build) void {
// In 0.16.0, the build graph environment holds the IO interface context
// const io = b.graph.io;
// Fetch the current time using the 'awake' clock (equivalent to old Timer)
//const start_time = std.Io.Clock.awake.now(io);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Create the executable artifact
const exe = b.addExecutable(.{
.name = "my-sdl3-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true, // FIX: This replaces linkLibC() in Zig 0.16.0
}),
});
// Fetch the zsdl3 dependency from build.zig.zon
const zsdl3_dep = b.dependency("zsdl3", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addIncludePath(b.path("src"));
exe.root_module.addCSourceFile(.{
.file = b.path("src/nuklear_impl.c"),
.flags = &.{"-std=c89"}, // Nuklear utilizes clean C89/ANSI C specification
});
// --- TRUE TIME TRACKING LOGIC ---
// 1. Allocate a persistent state block to pass timestamps between steps
// const TimeContext = struct {
// start: std.Io.Instant = undefined,
// };
// const ctx = b.allocator.create(TimeContext) catch unreachable;
// 2. Custom step executed BEFORE compilation starts
// const start_timer_step = b.allocator.create(std.Build.Step) catch unreachable;
// start_timer_step.* = std.Build.Step.init(.{
// .id = .custom,
// .name = "start_timer",
// .owner = b,
// .makeFn = struct {
// fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void {
// _ = options;
// const c: *TimeContext = @ptrCast(@alignCast(step.owner.graph.custom_data.?));
// c.start = std.Io.Clock.awake.now(step.owner.graph.io);
// }
// }.make,
// });
// b.graph.custom_data = ctx;
// 3. Custom step executed AFTER compilation finishes
// const end_timer_step = b.allocator.create(std.Build.Step) catch unreachable;
// end_timer_step.* = std.Build.Step.init(.{
// .id = .custom,
// .name = "end_timer",
// .owner = b,
// .makeFn = struct {
// fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void {
// _ = options;
// const c: *TimeContext = @ptrCast(@alignCast(step.owner.graph.custom_data.?));
// const elapsed = c.start.untilNow(step.owner.graph.io, .awake);
// const elapsed_ns: u64 = @intCast(elapsed.nanoseconds);
// const elapsed_ms = @divTrunc(elapsed_ns, std.time.ns_per_ms);
// std.debug.print("\n>>> [BUILD EXECUTION TIME] Compile took: {d} ms <<<\n\n", .{elapsed_ms});
// }
// }.make,
// });
// 4. Force the execution sequence via explicit graph dependencies
// start_timer -> compile exe -> end_timer
// exe.step.dependOn(start_timer_step);
// end_timer_step.dependOn(&exe.step);
// ----------------------------------------------------------
exe.root_module.addLibraryPath(b.path(".")); // root folder
// Bind zsdl3 to the executable's root module
exe.root_module.addImport("zsdl3", zsdl3_dep.module("zsdl3"));
// FIX: Methods belong directly to the Compile step 'exe', not 'root_module'
exe.root_module.linkSystemLibrary("SDL3", .{});
exe.root_module.linkSystemLibrary("SDL3_image", .{});
exe.root_module.linkSystemLibrary("SDL3_ttf", .{});
// exe.linkLibC();// 0.16.0 remove
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
// run_cmd.step.dependOn(end_timer_step);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
// Loop Through Your Manual Sample List
inline for (sample_list) |sample| {
const exe_name = b.fmt("zsample-{s}", .{sample.name});
const sample_exe = b.addExecutable(.{
.name = exe_name,
.root_module = b.createModule(.{
.root_source_file = b.path(sample.source),
.target = target,
.optimize = optimize,
.link_libc = true, // FIX: This replaces linkLibC() in Zig 0.16.0
}),
});
// Common plumbing linked automatically to every registered sample
sample_exe.root_module.addLibraryPath(b.path(".")); // root folder
sample_exe.root_module.addImport("zsdl3", zsdl3_dep.module("zsdl3"));
sample_exe.root_module.linkSystemLibrary("SDL3", .{});
sample_exe.root_module.linkSystemLibrary("SDL3_image", .{});
sample_exe.root_module.linkSystemLibrary("SDL3_ttf", .{});
// sample_exe.root_module.addImport("raygui", raygui);
// sample_exe.root_module.linkLibrary(zflecs_artifact);
// sample_exe.root_module.addImport("zflecs", zflecs_module);
b.installArtifact(sample_exe);
// Generate specific command string: "run-flecs", "run-third", etc.
const step_name = b.fmt("run-{s}", .{sample.name});
const step_desc = b.fmt("Run the {s} sample", .{sample.name});
const sample_run = b.addRunArtifact(sample_exe);
if (b.args) |args| sample_run.addArgs(args);
b.step(step_name, step_desc).dependOn(&sample_run.step);
}
// Compute the elapsed Io.Duration struct
// const elapsed = start_time.untilNow(io, .awake);
// 2. Extract the raw i96 nanoseconds field and cast it safely to u64
// const elapsed_ns: u64 = @intCast(elapsed.nanoseconds);
// 3. Divide down into milliseconds
// const elapsed_ms = @divTrunc(elapsed_ns, std.time.ns_per_ms);
// std.debug.print("[BUILD CONFIG TIME] Graph configured in {d} ms\n", .{elapsed_ms});
}
const std = @import("std");
const zsdl3 = @import("zsdl3");
const nk_sdl = @import("nuklear_sdl3.zig");
const nk = nk_sdl.nk;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
if (!zsdl3.init(zsdl3.SDL_INIT_VIDEO | zsdl3.SDL_INIT_EVENTS)) return;
defer zsdl3.quit();
if (!zsdl3.ttf.init()) return;
defer zsdl3.ttf.quit();
var win_width: c_int = 800;
var win_height: c_int = 600;
const window = zsdl3.createWindow("Responsive Canvas Workspace", win_width, win_height, zsdl3.SDL_WINDOW_RESIZABLE) orelse return;
defer zsdl3.destroyWindow(window);
const renderer = zsdl3.createRenderer(window, null) orelse return;
defer zsdl3.destroyRenderer(renderer);
const font_path = "assets/JetBrainsMonoNL-Light.ttf";
const font_size = 16;
const font = zsdl3.ttf.openFont(font_path, font_size) orelse return;
defer zsdl3.ttf.closeFont(font);
const max_memory = 4 * 1024 * 1024;
const nk_memory = try allocator.alloc(u8, max_memory);
defer allocator.free(nk_memory);
var ctx = try nk_sdl.initContext(nk_memory, font, @floatFromInt(font_size));
defer nk.nk_free(&ctx);
var running = true;
while (running) {
nk.nk_input_begin(&ctx);
var event: zsdl3.SDL_Event = undefined;
while (zsdl3.pollEvent(&event)) {
if (event.type == zsdl3.SDL_EVENT_QUIT) {
running = false;
}
if (event.type == zsdl3.SDL_EVENT_WINDOW_RESIZED) {
win_width = event.window.data1;
win_height = event.window.data2;
}
nk_sdl.handleEvent(&ctx, &event, window);
}
nk.nk_input_end(&ctx);
const panel_width = @as(f32, @floatFromInt(win_width)) - 100.0;
const panel_height = @as(f32, @floatFromInt(win_height)) - 100.0;
if (nk.nk_begin(&ctx, "Scalable Control Dock", nk.nk_rect(50, 50, panel_width, panel_height), nk.NK_WINDOW_BORDER | nk.NK_WINDOW_MOVABLE | nk.NK_WINDOW_SCALABLE | nk.NK_WINDOW_TITLE) != 0) {
nk.nk_layout_row_dynamic(&ctx, 20, 1);
nk.nk_label(&ctx, "Scroll Test Bench below:", nk.NK_TEXT_LEFT);
// Updated with explicit integer conversion type check != 0
if (nk.nk_group_begin(&ctx, "ScrollBox", nk.NK_WINDOW_BORDER) != 0) {
nk.nk_layout_row_dynamic(&ctx, 25, 1);
var i: usize = 1;
while (i <= 10) : (i += 1) {
var row_buf: [32]u8 = undefined;
const row_str = try std.fmt.bufPrintZ(&row_buf, "List Item Data Element #{d}", .{i});
nk.nk_label(&ctx, row_str.ptr, nk.NK_TEXT_LEFT);
}
nk.nk_group_end(&ctx);
}
nk.nk_layout_row_dynamic(&ctx, 20, 1);
var info_buf: [64]u8 = undefined;
const info_str = try std.fmt.bufPrintZ(&info_buf, "Canvas Size: {d}x{d}", .{win_width, win_height});
nk.nk_label(&ctx, info_str.ptr, nk.NK_TEXT_LEFT);
}
nk.nk_end(&ctx);
_ = zsdl3.setRenderDrawColor(renderer, 20, 24, 30, 255);
_ = zsdl3.renderClear(renderer);
nk_sdl.render(&ctx, renderer, font);
_ = zsdl3.renderPresent(renderer);
zsdl3.delay(16);
}
}
const std = @import("std");
const zsdl3 = @import("zsdl3");
pub const nk = @cImport({
@cInclude("nuklear.h");
});
/// Internal callback used by Nuklear to gauge rendered string widths via SDL_ttf
fn textWidthCallback(handle: nk.nk_handle, font_height: f32, text: [*c]const u8, len: c_int) callconv(.c) f32 {
const font_ptr: *const zsdl3.ttf.TTF_Font = @ptrCast(@alignCast(handle.ptr));
_ = font_height;
const slice = text[0..@intCast(len)];
var width: c_int = 0;
var height: c_int = 0;
const c_str: [*:0]const u8 = @ptrCast(slice.ptr);
if (zsdl3.ttf.getStringSize(font_ptr, c_str, slice.len, &width, &height)) {
return @floatFromInt(width);
}
return 0;
}
/// Initializes an idiomatic fixed Nuklear context bound to an active SDL_ttf Font reference
pub fn initContext(memory: []u8, font: *const zsdl3.ttf.TTF_Font, font_size: f32) !nk.nk_context {
// Statically configure user font structure metadata fields
var user_font: nk.nk_user_font = undefined;
user_font.height = font_size;
user_font.width = textWidthCallback;
user_font.userdata.ptr = @constCast(font);
var ctx: nk.nk_context = undefined;
if (nk.nk_init_fixed(&ctx, memory.ptr, memory.len, &user_font) == 0) {
return error.NuklearInitFailed;
}
return ctx;
}
/// Translates standard SDL3 Event structures directly down into Nuklear's internal layout states
pub fn handleEvent(ctx: *nk.nk_context, event: *const zsdl3.SDL_Event, window: *zsdl3.SDL_Window) void {
if (event.type == zsdl3.SDL_EVENT_MOUSE_MOTION) {
nk.nk_input_motion(ctx, @intFromFloat(event.motion.x), @intFromFloat(event.motion.y));
} else if (event.type == zsdl3.SDL_EVENT_MOUSE_BUTTON_DOWN or event.type == zsdl3.SDL_EVENT_MOUSE_BUTTON_UP) {
const down = if (event.type == zsdl3.SDL_EVENT_MOUSE_BUTTON_DOWN) @as(c_int, 1) else @as(c_int, 0);
const button = switch (event.button.button) {
1 => nk.NK_BUTTON_LEFT,
2 => nk.NK_BUTTON_MIDDLE,
3 => nk.NK_BUTTON_RIGHT,
else => nk.NK_BUTTON_MAX,
};
if (button != nk.NK_BUTTON_MAX) {
nk.nk_input_button(ctx, @as(c_uint, @intCast(button)), @intFromFloat(event.button.x), @intFromFloat(event.button.y), down);
}
}
// ── CATCH SDL3 MOUSE SCROLL WHEEL UPDATES ──
else if (event.type == zsdl3.SDL_EVENT_MOUSE_WHEEL) {
// Check if scrolling direction is inverted based on OS natural scroll properties
const is_flipped = event.wheel.direction == 1;
const scroll_y = if (is_flipped) -event.wheel.y else event.wheel.y;
// std.log.info("scroll", .{});
const scroll_vector = nk.nk_vec2(event.wheel.x, scroll_y);
nk.nk_input_scroll(ctx, scroll_vector);
}
// Track character typing streams
else if (event.type == zsdl3.SDL_EVENT_TEXT_INPUT) {
const text_ptr: [*c]const u8 = @ptrCast(event.text.text);
nk.nk_input_glyph(ctx, text_ptr);
}
// Track layout control strokes (Backspace, Enter, Arrows) using SDL3 nomenclature
else if (event.type == zsdl3.SDL_EVENT_KEY_DOWN or event.type == zsdl3.SDL_EVENT_KEY_UP) {
const down = if (event.type == zsdl3.SDL_EVENT_KEY_DOWN) @as(c_int, 1) else @as(c_int, 0);
// Explicitly compute the keycode mask value if zsdl3.SDLK_DELETE doesn't compile
const scancode_mask = (1 << 30);
const delete_keycode = @as(u32, @intCast(zsdl3.SDL_SCANCODE_DELETE)) | scancode_mask;
switch (event.key.key) {
zsdl3.SDLK_BACKSPACE => nk.nk_input_key(ctx, nk.NK_KEY_BACKSPACE, down),
zsdl3.SDLK_RETURN => nk.nk_input_key(ctx, nk.NK_KEY_ENTER, down),
zsdl3.SDLK_TAB => nk.nk_input_key(ctx, nk.NK_KEY_TAB, down),
zsdl3.SDLK_LEFT => nk.nk_input_key(ctx, nk.NK_KEY_LEFT, down),
zsdl3.SDLK_RIGHT => nk.nk_input_key(ctx, nk.NK_KEY_RIGHT, down),
zsdl3.SDLK_UP => nk.nk_input_key(ctx, nk.NK_KEY_UP, down),
zsdl3.SDLK_DOWN => nk.nk_input_key(ctx, nk.NK_KEY_DOWN, down),
else => |k| if (k == delete_keycode) {
nk.nk_input_key(ctx, nk.NK_KEY_DEL, down);
},
}
}
// ── DYNAMIC KEYBOARD TOGGLE CHECK ──
// If a text entry field is focused inside Nuklear, enable SDL's text processing engine
// if (ctx.input.text.active != 0) {
if (ctx.active != 0) {
_ = zsdl3.input.startTextInput(window);
} else {
_ = zsdl3.input.stopTextInput(window);
}
}
/// Loops through Nuklear's drawing queue and maps commands into native SDL3 draw updates
pub fn render(ctx: *nk.nk_context, renderer: *zsdl3.SDL_Renderer, font: *const zsdl3.ttf.TTF_Font) void {
var cmd = nk.nk__begin(ctx);
while (cmd != null) : (cmd = nk.nk__next(ctx, cmd)) {
switch (cmd.*.type) {
nk.NK_COMMAND_SCISSOR => {
const s = @as(*const nk.nk_command_scissor, @ptrCast(cmd));
const rect = zsdl3.SDL_Rect{
.x = @intCast(s.x),
.y = @intCast(s.y),
.w = @intCast(s.w),
.h = @intCast(s.h),
};
// Locks drawing operations strictly inside the UI panel boundary bounds
_ = zsdl3.setRenderClipRect(renderer, &rect);
},
nk.NK_COMMAND_RECT => {
const r = @as(*const nk.nk_command_rect, @ptrCast(cmd));
_ = zsdl3.setRenderDrawColor(renderer, r.color.r, r.color.g, r.color.b, r.color.a);
const rect = zsdl3.SDL_FRect{
.x = @floatFromInt(r.x),
.y = @floatFromInt(r.y),
.w = @floatFromInt(r.w),
.h = @floatFromInt(r.h),
};
_ = zsdl3.renderRect(renderer, &rect);
},
nk.NK_COMMAND_RECT_FILLED => {
const r = @as(*const nk.nk_command_rect_filled, @ptrCast(cmd));
_ = zsdl3.setRenderDrawColor(renderer, r.color.r, r.color.g, r.color.b, r.color.a);
const rect = zsdl3.SDL_FRect{
.x = @floatFromInt(r.x),
.y = @floatFromInt(r.y),
.w = @floatFromInt(r.w),
.h = @floatFromInt(r.h),
};
_ = zsdl3.renderFillRect(renderer, &rect);
},
nk.NK_COMMAND_TEXT => {
const t = @as(*const nk.nk_command_text, @ptrCast(cmd));
const raw_ptr: [*]const u8 = @ptrCast(&t.string);
const text_slice = raw_ptr[0..@intCast(t.length)];
const sdl_color = zsdl3.SDL_Color{ .r = t.foreground.r, .g = t.foreground.g, .b = t.foreground.b, .a = t.foreground.a };
const c_text: [*:0]const u8 = @ptrCast(text_slice.ptr);
if (zsdl3.ttf.renderTextBlended(font, c_text, text_slice.len, sdl_color)) |surface| {
defer zsdl3.destroySurface(surface);
if (zsdl3.createTextureFromSurface(renderer, surface)) |texture| {
defer zsdl3.destroyTexture(texture);
const dest_rect = zsdl3.SDL_FRect{
.x = @floatFromInt(t.x),
.y = @floatFromInt(t.y),
.w = @floatFromInt(surface.*.w),
.h = @floatFromInt(surface.*.h),
};
_ = zsdl3.renderTexture(renderer, texture, null, &dest_rect);
}
}
},
else => {},
}
}
nk.nk_clear(ctx);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment