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
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
| 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); | |
| } |