Posts
Search
Contact
Cookies
About
RSS

Nuklear (GUI) and raylib

Added 19 Oct 2020, 8:32 a.m. edited 18 Jun 2023, 1:45 p.m.

Nearly a year ago I demonstrated using Nuklear with raylib, however because I was rendering the GUI inside a 3D environment (on a face of a 3D model rather than as a simple 2D overlay) Its occurred to me recently that the implementation might be a little confusing for someone just wanting to use Nuklear in the normal 2D manner. I decided that it might be in order to make a simpler example, and while I was at it update the version of Nuklear I was using (this has meant I've had to patch Nuklear because it and raylib are both using stb_rect_pack.h causing an issue with linking)

It worth going over the specifics of using Nuklear, while I'm going to give a brief outline you probably should look at the documentation!

Lets look first at initialising the GUI ready for use.

    struct nk_context ctx;
    struct device dev;

    device_init(&dev);
    {
        const void *image; int w, h;
        struct nk_font *font;
        struct nk_font_atlas atlas;
        nk_font_atlas_init_default(&atlas);
        nk_font_atlas_begin(&atlas);
        font = nk_font_atlas_add_default(&atlas, 18.0f, NULL);
        image = nk_font_atlas_bake(&atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
        device_upload_atlas(&dev, image, w, h);
        nk_font_atlas_end(&atlas, nk_handle_id((int)dev.font_tex), &dev.null);
        nk_init_default(&ctx, &font->handle);
    }

This is really just boiler plate code, the only thing that you might want to change is on line 6, which determines the font size. When updating just before rendering it is necessary to send input events to Nuklear, I've provided a function to do this in rayNuk.c

void pumpInput(struct nk_context *ctx, Vector2 mp) 

This takes the Nuclear "context" (basically a struct of global variable) and the position of the mouse pointer, if you are scaling your screen to preserve aspect ratio when it resizes you must remember to scale this value too.

actually rendering the GUI is straight forward

            // setup the gui widgets for render
            if (nk_begin(&ctx, "a window", nk_rect(10, 10, 400, 440),
                NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|NK_WINDOW_CLOSABLE))
            {
                doSlider(&ctx,"X angle:",&xang,0.0,359.0);
                doSlider(&ctx,"Y angle:",&yang,0.0,359.0);
                doSlider(&ctx,"Z angle:",&zang,0.0,359.0);
            } else {
                exit = true;
            }

            nk_end(&ctx);
            // actually render the gui
            device_draw(&dev, &ctx, screenWidth, screenHeight, nk_vec2(1,1), NK_ANTI_ALIASING_ON);

Nuklear uses nk_begin and nk_end to define a window, all the calls to define widgets for that window must be between those two calls. There is often a lot of repeated code with Nuklear GUI's so I made a simple helper function to make a labelled slider which we'll look at next, before we do notice the nk_begin is in an if statement, if the window returns false its been closed, so I've chosen to signal to the main look that the program should exit. Once Nuklear has built a list of draw commands representing the current state of the GUI, you can go ahead and actually render it with device_draw

void doSlider(struct nk_context* ctx,char* label ,float* var,float vmin, float vmax) 
{
    nk_layout_row_begin(ctx, NK_STATIC, 24, 2);
    nk_layout_row_push(ctx, 180);
    char str[256];
    sprintf(str, "%s %f:",label,*var);
    nk_label(ctx, str, NK_TEXT_LEFT);
    nk_layout_row_push(ctx, 160);
    nk_slider_float(ctx, vmin, var, vmax, .01f);
    nk_layout_row_end(ctx);
}

This convenience function in the example places a labelled slider on a row of its own (look at the documentation for various other way to layout a GUI) with nk_layout_row_push defining columns within the row. Notice that a pointer to a variable is sent with nk_slider_float (var) this variable is directly effected when the user interacts with the slider.

You can download a complete ready to compile example here