Hello everybody,
I am trying to write a little GUI, using raylib and clay, both using the zig wrappers
- GitHub - raylib-zig/raylib-zig: Manually tweaked, auto-generated raylib bindings for zig. https://github.com/raysan5/raylib
- GitHub - johan0A/clay-zig-bindings: Zig bindings for the library clay: A high performance UI layout library in C.
Now I am in this situation:
The following is my main zig file. It sets up raylib, as well as clay, and then renders a UI, that lists a bunch of songs in a vertical, scrollable container, each being named Song {i}. I do this by first allocating a string, with the appropriate index number and then pushing that to clay. (I know this leaks memory as hell, but I want to understand why this behaves the way it does, before fixing it). The UI renders correctly, with each list entry having its appropriate index.
I also set up a click hander with clay.onHover, to which I pass the clicked element string as a pointer to the string slice.
Now the problem: Inside the click handler it always receives the pointer to the last slice. So Song 49.
const std = @import("std");
const builtin = @import("builtin");
const raylib = @import("raylib");
const clay = @import("zclay");
const rl_renderer = @import("raylib_render_clay.zig");
const WINDOW_WIDTH = 1000;
const WINDOW_HEIGHT = 1000;
const TARGET_FPS = 120;
fn loadFont(file_data: ?[]const u8, font_id: u16, font_size: i32) !void {
rl_renderer.raylib_fonts[font_id] = try raylib.loadFontFromMemory(".ttf", file_data, font_size * 2, null);
raylib.setTextureFilter(rl_renderer.raylib_fonts[font_id].?.texture, .bilinear);
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
// init clay
const min_memory_size: u32 = clay.minMemorySize();
const memory = try allocator.alloc(u8, min_memory_size);
defer allocator.free(memory);
const arena: clay.Arena = clay.createArenaWithCapacityAndMemory(memory);
_ = clay.initialize(arena, .{ .h = WINDOW_HEIGHT, .w = WINDOW_WIDTH }, .{});
clay.setMeasureTextFunction(void, {}, rl_renderer.measureText);
// init raylib
raylib.setConfigFlags(.{
.msaa_4x_hint = true,
.window_resizable = true,
});
raylib.initWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Raylib zig Example");
raylib.setTargetFPS(TARGET_FPS);
// load assets
try loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 0, 24);
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
const alloc, const is_debug = gpa: {
break :gpa switch (builtin.mode) {
.Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
.ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
};
};
defer if (is_debug) {
_ = debug_allocator.deinit();
};
var debug_mode_enabled = false;
while (!raylib.windowShouldClose()) {
if (raylib.isKeyPressed(.d)) {
debug_mode_enabled = !debug_mode_enabled;
clay.setDebugModeEnabled(debug_mode_enabled);
}
clay.setLayoutDimensions(.{
.w = @floatFromInt(raylib.getScreenWidth()),
.h = @floatFromInt(raylib.getScreenHeight()),
});
const mouse_pos = raylib.getMousePosition();
clay.setPointerState(.{
.x = mouse_pos.x,
.y = mouse_pos.y,
}, raylib.isMouseButtonDown(.left));
const scroll_delta = raylib.getMouseWheelMoveV().multiply(.{ .x = 6, .y = 6 });
clay.updateScrollContainers(
false,
.{ .x = scroll_delta.x, .y = scroll_delta.y },
raylib.getFrameTime(),
);
clay.beginLayout();
clay.UI()(.{
.id = .ID("MainContainer"),
.layout = .{
.sizing = .grow,
.child_alignment = .{
.x = .center,
.y = .top,
},
},
.background_color = .{255, 255, 255, 255},
})({
clay.UI()(.{
.id = .ID("SongContainer"),
.background_color = .{0, 0, 0, 255},
.layout = .{
.sizing = .fit,
.direction = .top_to_bottom,
.child_gap = 16,
},
.clip = .{
.vertical = true,
.child_offset = clay.getScrollOffset(),
},
})({
// PART IN QUESTION
for (0..50) |i| {
const name = std.fmt.allocPrint(alloc, "Song {d}", .{i}) catch continue;
clay.UI()(.{
.id = .ID(name),
.layout = .{
.sizing = .fit,
},
})({
clay.onHover(*const []u8, &name, button_interaction);
clay.text(name, .{ .font_size = 24, .color = .{255, 255, 255, 255} });
});
}
// PART IN QUESTION
});
});
const render_commands = clay.endLayout();
raylib.beginDrawing();
try rl_renderer.clayRaylibRender(render_commands, allocator);
raylib.endDrawing();
}
}
const Test = struct {
name: []u8,
};
fn button_interaction(elem: clay.ElementId, pointer: clay.PointerData, user_data: *const []u8) void {
_ = elem;
if (pointer.state == .pressed_this_frame) {
std.debug.print("clicked element: {s}\n", .{user_data.*});
}
}
You can see the output below. It always ends up being the same name.
~/code/lauscher [nix: nix-lauscher-dev-env]
130 » zig build -freference-trace=10 run
...
clicked element: Song 49
clicked element: Song 49
clicked element: Song 49
After experimenting a bit, I came up with this construct. I create a wrapper struct for the string slice on the heap, passing a pointer to that, which is then dereferenced and the name is printed.
for (0..50) |i| {
const name = std.fmt.allocPrint(alloc, "Song {d}", .{i}) catch continue;
const t: *Test = alloc.create(Test) catch unreachable;
t.name = name;
clay.UI()(.{
.id = .ID(name),
.layout = .{
.sizing = .fit,
},
})({
clay.onHover(*Test, t, button_interaction);
clay.text(name, .{ .font_size = 24, .color = .{255, 255, 255, 255} });
});
}
fn button_interaction(elem: clay.ElementId, pointer: clay.PointerData, user_data: *Test) void {
_ = elem;
if (pointer.state == .pressed_this_frame) {
std.debug.print("clicked element: {s}\n", .{user_data.name});
}
}
As you can see, this works.
~/code/lauscher [nix: nix-lauscher-dev-env]
130 » zig build -freference-trace=10 run zeno@apokory | 8:47:57
...
clicked element: Song 12
clicked element: Song 11
clicked element: Song 10
Now, why does that work? In my understanding, both are pointers to heap structures, or at least pointers to memory that lives longer, then the frame, as it is never freed.
And how can I built this, so I dont have to wrap the strings in a separate structure?
If need be, I can put everything in a git repo, together with a nix flake, if that makes things easier to debug.
Thank you