nicbarker / clay

High performance UI layout library in C.
https://nicbarker.com/clay
zlib License
1.28k stars 30 forks source link

[Help Wanted] Zig & Odin #3

Open nicbarker opened 3 months ago

nicbarker commented 3 months ago

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!

Dudejoe870 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

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 commented 2 months ago

@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 commented 2 months ago

I've made some great progress with the Odin bindings today: https://github.com/nicbarker/clay/pull/5

crystalthoughts commented 2 months ago

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 commented 2 months ago

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

nicbarker commented 2 months ago

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!

Srekel commented 3 weeks ago

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 commented 3 weeks ago

@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 https://github.com/nicbarker/clay/pull/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.