Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Help Wanted] Zig & Odin #3

Open
nicbarker opened this issue Aug 27, 2024 · 15 comments
Open

[Help Wanted] Zig & Odin #3

nicbarker opened this issue Aug 27, 2024 · 15 comments
Labels
help wanted Extra attention is needed

Comments

@nicbarker
Copy link
Owner

I would love for clay to be easily usable from both Zig and Odin.
However, there's a major sticking point with both that I don't know how to solve.

The core Element Macros rely on a particular feature of the C preprocessor, specifically "function-like macros" and the ability to pass arbitrary text as an argument to these macros. If you're not familiar with this in C, you can take a look at the definition of any of these macros in clay.h (example) and you'll see that they all follow a general form:

#define CLAY_CONTAINER(id, layoutConfig, children)
    Clay__OpenContainerElement(id, layoutConfig);
    children
    Clay__CloseContainerElement()

You can see that in order to correctly construct the layout hierarchy, child declarations need to preceded by a Clay__Open... and then followed by a Clay__Close....
In clay's use case, these macros allows you to automatically sandwhich child layout elements in the required open / close, by "passing a block as a macro argument" - creating (imo) a very nice developer experience:

CLAY_CONTAINER(id, layout, { // This {} is the "third argument" to the macro
    CLAY_TEXT(id, "Child text", layout);
    CLAY_CONTAINER(id, layout, {
        // ... etc
    });
});

As a result it's not actually possible to "forget" to close these containers and end up with a mismatch or with elements incorrectly parented - this macro syntax functions almost like a form of RAII.

Neither Zig nor Odin support this type of "function-like macro" where arbitrary text can be pasted in.

In Odin's case, it might be possible through some combination of defer and non capturing lambdas to replicate this type of behaviour, but what I'm really looking for is something fool proof - where you don't have to spend time debugging a missing call to Clay__Close..., and I don't have to build debug tools to help you with that 😛

In Zig's case, AFAIK there is even less official support for closures, and just glossing over the docs I can't really think of a way to implement it that wouldn't make layout definition a mess.

Any help or out of the box ideas would be greatly appreciated!

@nicbarker nicbarker added the help wanted Extra attention is needed label Aug 27, 2024
Repository owner deleted a comment Aug 27, 2024
Repository owner deleted a comment Aug 27, 2024
@Dudejoe870
Copy link
Contributor

For Odin:
You do actually have some amount of support for this, for example, vendor:microui uses a special attribute to allow you to do

if window(...) { }

instead of

if begin_window(...) {
    defer end_window()
}

an example from the source code how such a function is defined:

@(deferred_in_out=scoped_end_window)
window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{}) -> bool {
	return begin_window(ctx, title, rect, opt)
}

scoped_end_window :: proc(ctx: ^Context, _: string, _: Rect, _: Options, ok: bool) {
	if ok {
		end_window(ctx)	
	}
}

now, I see that these macros don't actually have return values, but you could certainly leverage this behavior anyway, to make fairly nice to use Odin bindings (which would be great, someone just posted this library in the Odin Discord, and it looks really useful!)

@nicbarker
Copy link
Owner Author

Just for posterity the folks over at the Odin discord have been very helpful, and I'm going to have a crack at writing the Odin bindings today.

@illfygli
Copy link

illfygli commented Aug 27, 2024

I'm not an expert, but here's a Zig idea I toyed with:

const std = @import("std");
const LayoutConfig = struct {};
// Would be the real imported Clay
const clay_ns = @This();

pub fn clay(layout: anytype) void {
    const fields = std.meta.fields(@TypeOf(layout));

    inline for (fields) |el| {
        comptime var name: [el.name.len]u8 = undefined;
        @memcpy(&name, el.name);
        // Crudely capitalize the field name.
        name[0] = comptime std.ascii.toUpper(name[0]);

        const openFn = @field(clay_ns, "Clay__Open" ++ name ++ "Element");
        const closeFn = @field(clay_ns, "Clay__Close" ++ name ++ "Element");

        // The Clay element functions have different arities,
        // so more code would be needed here.
        const args = @field(layout, el.name);
        const id_arg = args[0];
        const config_arg = args[1];

        openFn(id_arg, config_arg);
        defer closeFn();

        if (3 <= args.len) {
            clay(args[2]);
        }
    }
}

const id = 123;

pub fn main() void {
    clay(.{
        .container = .{
            id,
            LayoutConfig{},
            .{
                .text = .{
                    id,
                    "text goes here",
                },
                .rectangle = .{
                    id,
                    LayoutConfig{},
                },
            },
        },
    });
}

fn Clay__OpenContainerElement(id_: usize, layout_config: LayoutConfig) void {
    std.log.debug("open container element {d} {any}", .{ id_, layout_config });
}

fn Clay__CloseContainerElement() void {
    std.log.debug("close container element", .{});
}

fn Clay__OpenTextElement(id_: usize, text: []const u8) void {
    std.log.debug("open text element {d} {s}", .{ id_, text });
}

fn Clay__CloseTextElement() void {
    std.log.debug("close text element", .{});
}

fn Clay__OpenRectangleElement(id_: usize, layout_config: LayoutConfig) void {
    std.log.debug("open rectangle element {d} {any}", .{ id_, layout_config });
}

fn Clay__CloseRectangleElement() void {
    std.log.debug("close rectangle element", .{});
}

Running this program prints:

debug: open container element 123 test.LayoutConfig{ }
debug: open text element 123 text goes here
debug: close text element
debug: open rectangle element 123 test.LayoutConfig{ }
debug: close rectangle element
debug: close container element

So you get the sandwiching, but it would need more work, and I don't know if it's a good direction.

@nicbarker
Copy link
Owner Author

@illfygli That is super cool, thanks so much for the code samples! I will do some investigation when I have time, would be great to get it working from Zig.

@nicbarker
Copy link
Owner Author

I've made some great progress with the Odin bindings today: #5

@crystalthoughts
Copy link

crystalthoughts commented Aug 28, 2024

I'd love to see Nim bindings too, the macro system will allow for the functional form :)

Edit: Missed the part where you explain how the macros work, should be simple to map to other DSL forms! I might take a crack if there are no takers

@nicbarker
Copy link
Owner Author

@crystalthoughts Please feel free, I've never used nim myself but it looks cool 😁

@nicbarker
Copy link
Owner Author

Now that the odin bindings are out the door and I've got a reasonable idea of how long it takes to write them for another language, I'm probably going to delay writing the zig bindings until after I've finished some feature work. Still high on the priority list, though!

@nicbarker nicbarker changed the title Help Wanted: Zig & Odin [Help Wanted] Zig & Odin Sep 15, 2024
@Srekel
Copy link

Srekel commented Nov 3, 2024

Very cool! We would love Zig bindings for Tides of Revival, though us needing complex UI is a bit off in the future, so there's no urgency on our part. But please ping me if you think we can help :)

image

@nicbarker
Copy link
Owner Author

@Srekel that's great to hear, Tides looks like an awesome and ambitious project 😁 Now that I've landed the majority of the planned breaking changes in #34, I feel a bit more confident making an attempt on the Zig bindings, and will likely ask in the language discord server for help.

@AYM1607
Copy link

AYM1607 commented Dec 21, 2024

Hey @nicbarker, I just watched your YT video and clay looks great! I have a Zig project that would benefit from a nice UI and I'd love to pitch in if you need any help with the bindings when you come around to it

@Zettexe
Copy link
Contributor

Zettexe commented Dec 28, 2024

Since I imagine @nicbarker is busy with other tasks I have started work on my own clay bindings for zig at https://codeberg.org/Zettexe/clay-zig. Keep in mind that they are currently very unfinished so it may still be a while until they are usable.

@raugl
Copy link

raugl commented Dec 31, 2024

Funnily enough, I have also started on a set of bindings for zig after watching that video. They are at https://github.com/raugl/clay-zig. You can use it today, though it still needs examples and more decoupling from raylib. It is only a few commits behind master and it needs zig 0.14 for the more compact api, but can be easily made to work on 0.13 too.

I have ditched the more imperative api that is used internally by the macros, and which is replicated by most of the other bindings, for a more declarative one with a single ElementConfig struct that is later used to attach all the specified properties. I have also added a way for hover() to work without requiring explicit ids.

@QuentinTessier
Copy link

QuentinTessier commented Jan 9, 2025

Here is my idea for the zig binding. I went for something close to @illfygli, I wanted to keep the automatic close of the element. There is a bit of boiler plate comptime code to capture function and generic arguments.

Example code

pub fn main() !void {
    var value: i32 = 0;
    const sub_scoped_object = Closure.closure(.{@as(*i32, &value)}, struct {
        pub fn inline_fn(single_arg: *i32) void {
            std.log.info("Inside captured generic code", .{});
            single_arg.* = 10;
            clay.clay(clay.child(&.{}, &.{})); // More ui code in a
        }
    }.inline_fn);

    clay.clay(
        clay.child(&.{
            clay.RectangleElement{},
            CClay.Clay_RectangleElementConfig{},
        }, &.{
            clay.child(
                &.{@as(u32, 1)},
                &.{sub_scoped_object},
            ),
        }),
    );

    std.log.info("Edited value: {}", .{value});
}

Here is the output from the code above:

info: Open Element (*const struct { comptime main.clay.RectangleElement = .{} }:*const struct { main.clay.Child(*const struct { comptime u32 = 1 },*const struct { Closure.Closure(struct { *i32 },fn (*i32) void) }) })
info: Zig Rectangle
info: C Rectangle
info: Open Element (*const struct { comptime u32 = 1 }:*const struct { Closure.Closure(struct { *i32 },fn (*i32) void) })
info: Unknown u32
info: Inside captured generic code
info: Open Element (@TypeOf(.{}):@TypeOf(.{}))
info: End (@TypeOf(.{}):@TypeOf(.{}))
info: End (*const struct { comptime u32 = 1 }:*const struct { Closure.Closure(struct { *i32 },fn (*i32) void) })
info: End (*const struct { comptime main.clay.RectangleElement = .{} }:*const struct { main.clay.Child(*const struct { comptime u32 = 1 },*const struct { Closure.Closure(struct { *i32 },fn (*i32) void) }) })
info: Edited value: 10

See this Gits for the full impl of Closure
https://gist.github.com/QuentinTessier/74bf910fa6ba9e724d19d57a4f06fba3

@Zettexe
Copy link
Contributor

Zettexe commented Jan 10, 2025

Since I imagine @nicbarker is busy with other tasks I have started work on my own clay bindings for zig at https://codeberg.org/Zettexe/clay-zig. Keep in mind that they are currently very unfinished so it may still be a while until they are usable.

Its taken a little bit but my bindings are now functional, the only completed renderer is the raylib one but ill add more soon and using it is completely optional. I found a way to kind of abuse the way Zig handles the execution order of blocks to create a syntax like this:

clay.ui()(.{
    .id = clay.id("SideBar"),
    .layout = .{
        .direction = .TopToBottom,
        .alignment = .{ .x = .Center, .y = .Top },
        .sizing = .{ .width = clay.Sizing.Axis.fixed(300), .height = clay.Sizing.Axis.grow },
        .padding = .{ .x = 16, .y = 16 },
        .child_gap = 16,
    },
})({
 // Body here
});

Since I only implemented functions as I was trying to make the raylib example work ive probably missed something so feel free to give me feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

10 participants