Skip to content

Instantly share code, notes, and snippets.

@Lightnet
Last active June 29, 2026 06:56
Show Gist options
  • Select an option

  • Save Lightnet/257474fe40a91b3aa644920290bb39af to your computer and use it in GitHub Desktop.

Select an option

Save Lightnet/257474fe40a91b3aa644920290bb39af to your computer and use it in GitHub Desktop.
Raylib 6.0 and nuklear 4.13.3 with zig 0.16.0 sample test by using the AI model to quick setup.

raylib and nuklear

Simple sample test with AI to handle setup and how it works.

project root
- assets
  - JetBrainsMonoNL-Light.ttf
- src
  - nuklear_impl.c
  - raylib_nk.zig
- build.zig

build.zig.zon

.raylib_zig = .{
            .url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#97be2c7ae646a2f09856604b138c427c0af1b952",
            .hash = "raylib_zig-6.0.0-KE8REDJ4BQBu7lKwYNt6R4j5ywPeS37zqW6zrtjCcRwH",
        },
//
.nuklear = .{
            .url = "https://github.com/Immediate-Mode-UI/Nuklear/archive/refs/tags/v4.13.3.zip",
            .hash = "N-V-__8AAI1oZADGeQpzQi4m7ubWPpcqb1DL_5BQimDkENNv",
        },

Note that nuklear use the different command:

zig fetch --save=nuklear https://github.com/Immediate-Mode-UI/Nuklear/archive/refs/tags/v4.13.3.zip
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 = "third", .source = "tests/sample_third.zig" },
// .{ .name = "transparent", .source = "tests/sample_transparent_window.zig" },
// .{ .name = "gui", .source = "tests/sample_gui.zig" },
.{ .name = "text3d", .source = "tests/sample_rl_3d_text.zig" },
.{ .name = "text01", .source = "tests/sample_plane_text.zig" },
.{ .name = "text02", .source = "tests/sample_plane_text02.zig" },
// .{ .name = "rlgl", .source = "tests/sample_rlgl.zig" },
// .{ .name = "future", .source = "src/sample_future.zig" },
//
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// 1. Core Dependencies Setup
const raylib_dep = b.dependency("raylib_zig", .{ .target = target, .optimize = optimize });
const raylib = raylib_dep.module("raylib");
// const rlgl = raylib_dep.module("rlgl");
const raygui = raylib_dep.module("raygui");
const raylib_artifact = raylib_dep.artifact("raylib");
// Add this macro definition to expose RLGL functionality
// raylib_artifact.root_module.addCMacro("SUPPORT_MODULE_RLGL", "1");
const zflecs = b.dependency("zflecs", .{ .target = target, .optimize = optimize });
const zflecs_module = zflecs.module("root");
const zflecs_artifact = zflecs.artifact("flecs");
// Fetch the raw C source files via the package manager
const nuklear_dep = b.dependency("nuklear", .{
.target = target,
.optimize = optimize,
});
// 2. Main Application
const exe = b.addExecutable(.{
.name = "ziraylib",
.root_module = b.createModule(.{
// .root_source_file = b.path("src/main.zig"),
.root_source_file = b.path("src/raylib_nk.zig"),
.target = target,
.optimize = optimize,
}),
});
exe.root_module.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);
exe.root_module.addImport("raygui", raygui);
exe.root_module.linkLibrary(zflecs_artifact);
exe.root_module.addImport("zflecs", zflecs_module);
// Inject Nuklear C headers directly into your executable module paths
exe.root_module.addIncludePath(nuklear_dep.path(""));
// --- ADD THESE TWO LINES ---
// Compile the implementation as native C, ensuring it has access to its own headers
exe.root_module.addCSourceFile(.{ .file = b.path("src/nuklear_impl.c") });
exe.root_module.linkSystemLibrary("c", .{});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| run_cmd.addArgs(args);
b.step("run", "Run the main app").dependOn(&run_cmd.step);
// 3. 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,
}),
});
// Common plumbing linked automatically to every registered sample
sample_exe.root_module.linkLibrary(raylib_artifact);
sample_exe.root_module.addImport("raylib", raylib);
// sample_exe.root_module.addImport("rlgl", rlgl);
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);
}
}
// src/nuklear_impl.c
#define NK_IMPLEMENTATION
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_DEFAULT_FONT // <-- ADD THIS LINE
#include "nuklear.h"
const std = @import("std");
const rl = @import("raylib");
// Global or in main
var font: rl.Font = undefined;
// Import Nuklear cleanly from your cached dependency include path
// Clear out implementation macros; look up headers only
const nk = @cImport({
@cDefine("NK_INCLUDE_FIXED_TYPES", "");
@cDefine("NK_INCLUDE_DEFAULT_ALLOCATOR", "");
@cInclude("nuklear.h");
});
// Callback wrapper to measure string pixel widths using Raylib's default metrics
// fn textWidthCallback(handle: nk.nk_handle, height: f32, text: [*c]const u8, len: c_int) callconv(.c) f32 {
// _ = handle;
// _ = height;
// // 1. Create a safe slice from the raw text pointer
// const slice = text[0..@intCast(len)];
// // 2. Prepare a 256-byte stack buffer (plenty for standard UI text lengths)
// var buf: [256]u8 = undefined;
// if (slice.len >= buf.len) return 0.0; // Fail-safe boundary check
// // 3. Copy the string data and null-terminate it manually
// @memcpy(buf[0..slice.len], slice);
// buf[slice.len] = 0;
// // 4. Create a valid sentinel-terminated slice for Raylib
// const safe_text: [:0]const u8 = buf[0..slice.len :0];
// return @floatFromInt(rl.measureText(safe_text, 14));
// }
fn textWidthCallback(handle: nk.nk_handle, height: f32, text: [*c]const u8, len: c_int) callconv(.c) f32 {
_ = handle;
// _ = height;
if (len <= 0) return 0.0;
const slice = text[0..@intCast(len)];
var buf: [512:0]u8 = undefined;
if (slice.len >= buf.len) return 0.0;
@memcpy(buf[0..slice.len], slice);
buf[slice.len] = 0;
const safe_text: [:0]const u8 = buf[0..slice.len :0];
// const size = rl.measureTextEx(font, safe_text, 18.0, 0); // ← use same size as nk_font.height
const size = rl.measureTextEx(font, safe_text, height, 0); // ← use same size as nk_font.height
return size.x;
}
pub fn main() !void {
const screenWidth = 800;
const screenHeight = 450;
rl.initWindow(screenWidth, screenHeight, "Raylib + Nuklear v4.13.3");
defer rl.closeWindow();
// Option A: Load from file (TTF, OTF, etc.)
// font = rl.loadFont("assets/JetBrainsMonoNL-Light.ttf");
font = try rl.loadFont("assets/JetBrainsMonoNL-Light.ttf");
rl.setTextureFilter(font.texture, .bilinear);// this handle text render correctly. Without it will blur text.
defer rl.unloadFont(font);
rl.setTargetFPS(60);
// New safe manual initialization block
var user_font: nk.nk_user_font = undefined;
user_font.userdata = .{ .ptr = null };
user_font.height = 18.0;
user_font.width = textWidthCallback;
var ctx: nk.nk_context = undefined;
_ = nk.nk_init_default(&ctx, &user_font);
defer nk.nk_free(&ctx);
while (!rl.windowShouldClose()) {
// Handle input state...
nk.nk_input_begin(&ctx);
const mouse_pos = rl.getMousePosition();
nk.nk_input_motion(&ctx, @intFromFloat(mouse_pos.x), @intFromFloat(mouse_pos.y));
nk.nk_input_button(&ctx, nk.NK_BUTTON_LEFT, @intFromFloat(mouse_pos.x), @intFromFloat(mouse_pos.y), @intFromBool(rl.isMouseButtonDown(.left)));
nk.nk_input_end(&ctx);
// GUI Window declaration
if (nk.nk_begin(&ctx, "Nuklear Window", nk.nk_rect(50, 50, 230, 250), nk.NK_WINDOW_BORDER | nk.NK_WINDOW_MOVABLE | nk.NK_WINDOW_TITLE) != 0) {
nk.nk_layout_row_static(&ctx, 30, 80, 1);
if (nk.nk_button_label(&ctx, "Test Zip") != 0) {
std.debug.print("Compiled directly from the raw Zip target!\n", .{});
}
}
nk.nk_end(&ctx);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(.ray_white);
// Primitive Draw Command Loop
// Iterate through Nuklear's draw commands and render them with Raylib
// Render Nuklear commands
var cmd_iter = nk.nk__begin(&ctx);
while (cmd_iter != null) : (cmd_iter = nk.nk__next(&ctx, cmd_iter)) {
const cmd = cmd_iter.*; // base command header
switch (cmd.type) {
nk.NK_COMMAND_NOP => {},
nk.NK_COMMAND_SCISSOR => {
// Optional: implement scissor if you need proper clipping
const c = @as(*const nk.nk_command_scissor, @ptrCast(cmd_iter));
// rl.BeginScissorMode(...) or similar
_ = c;
},
nk.NK_COMMAND_RECT => { // unfilled (border)
const c = @as(*const nk.nk_command_rect, @ptrCast(cmd_iter));
rl.drawRectangleLinesEx(rl.Rectangle{ .x = @floatFromInt(c.x), .y = @floatFromInt(c.y), .width = @floatFromInt(c.w), .height = @floatFromInt(c.h) }, @floatFromInt(c.line_thickness), rl.Color{ .r = c.color.r, .g = c.color.g, .b = c.color.b, .a = c.color.a });
},
nk.NK_COMMAND_RECT_FILLED => {
const c = @as(*const nk.nk_command_rect_filled, @ptrCast(cmd_iter));
rl.drawRectangle(@intCast(c.x), @intCast(c.y), @intCast(c.w), @intCast(c.h), rl.Color{ .r = c.color.r, .g = c.color.g, .b = c.color.b, .a = c.color.a });
},
nk.NK_COMMAND_TEXT => {
const c = @as(*const nk.nk_command_text, @ptrCast(cmd_iter));
if (c.length > 0) {
const text_ptr: [*]const u8 = @ptrCast(&c.string);
const slice = text_ptr[0..@intCast(c.length)];
var buf: [512:0]u8 = undefined;
if (slice.len >= buf.len) continue;
@memcpy(buf[0..slice.len], slice);
buf[slice.len] = 0;
const safe_text: [:0]const u8 = buf[0..slice.len :0];
const position = rl.Vector2{ .x = @floatFromInt(c.x), .y = @floatFromInt(c.y) };
const color = rl.Color{ .r = c.foreground.r, .g = c.foreground.g, .b = c.foreground.b, .a = c.foreground.a };
// FIX: Pass c.height directly since it is already an f32
// rl.drawTextEx(font, safe_text, position, c.height, 0.0, color);
rl.drawTextEx(font, safe_text, position, c.height, 0.0, color);
// rl.drawTextEx(font, safe_text, position, 24, 0.0, color);
}
},
// nk.NK_COMMAND_TEXT => {
// const c = @as(*const nk.nk_command_text, @ptrCast(cmd_iter));
// // Safer text extraction
// if (c.length > 0) {
// const text_ptr: [*]const u8 = @ptrCast(&c.string);
// const slice = text_ptr[0..@intCast(c.length)];
// // Stack buffer + null termination for Raylib
// var buf: [512:0]u8 = undefined; // generous size
// if (slice.len >= buf.len) {
// // truncate or skip very long text
// continue;
// }
// @memcpy(buf[0..slice.len], slice);
// buf[slice.len] = 0;
// const safe_text: [:0]const u8 = buf[0..slice.len :0];
// rl.drawText(safe_text, @intCast(c.x), @intCast(c.y), 14, // or use @intFromFloat(c.height)
// rl.Color{
// .r = c.foreground.r,
// .g = c.foreground.g,
// .b = c.foreground.b,
// .a = c.foreground.a,
// });
// }
// },
// Add more as needed (circles, lines, images, etc.)
else => {},
}
}
nk.nk_clear(&ctx);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment