Nuklear GUI Usage Guide and Tutorial (for LWJGL)

A look at the open source library for building Graphical User Interfaces.

I was building an application in Java using LWJGL and was in need of a user interface that would work in conjuction with OpenGL. The option that LWJGL provides is the Nuklear library. They show quite impressive demos on their Github page, but when it came to using the library I found that I could not do anything that was not shown in demo code because most of the functions are not properly documented. The only information we have about them is the name of the function and the data type of the function's parameters.

After a whole lot of experimentation and digging through the source code I have gotten a good enough understanding of Nuklear to make practical use of it. In order to save people from having to go through the trouble I did, I am creating this guide. The following guide is written for the LWJGL binding of Nuklear in Java, some of the function parameter counts may differ from the C version, but a lot of the information in here should still be useful even for other languages.

The Nuklear library has hundreds of functions, so please forgive me for not documenting everything. I am trying to document enough of it for almost all practical uses. If something is unclear or incorrect, please send me an e-mail.

For the course of this document, I will be using the property context to refer to the NkContext object used in all Nuklear functions. (The LWJGL demo code uses ctx instead.)

Updates

I will update this guide with more widgets, details and corrections when I get the time. Keep track of it here.

  1. 2018-08-17
    • First iteration of the Nuklear GUI Usage Guide and Tutorial.
    • Description of all layout elements: Windows, rows and groups
    • Reference entries for the following widgets: Label, text field, button
    • Function reference entries for all functions used by layout elements and the documented widgets.
    • Struct reference entries for all structs used by layout elements and the documented widgets.
  2. 2020-06-04
    • Separated the Struct Reference and Function Reference into their own pages.
    • Added a reference entry for the number field widgets.
    • Updated page styles for improved legibility.

Setting It All Up

This is necessary but not the focus of this document, so please skip this section if you're able to set up a Nuklear context without help. You can copy the code for the setup from the LWJGL demo, I am just giving a brief explanation here for each section of that code.

Nuklear is built to be completely platform independent, so it does not actually do any of the work of listening for mouse and keyboard events or drawing to the screen, instead you have to involve other libraries to do that work. For this reason the setup is a bit tedious.

For setup we will be using the following libraries:

For this guide, these libraries will no longer be used after setup is complete, so you do not need to know much about them, but I highly recommend getting familiar with OpenGL since you probably will be using it in other places.

All of the code in this section is provided in the LWJGL demo. I am basically copying it as is and adding a few comments for your understanding. If you can get that demo running then you can skip over this section.

For convenience, we should import Nuklear and other C functions statically as in the following example.

import static org.lwjgl.nuklear.Nuklear.*;

To begin, we initialize variables that are used throughout the whole process:

    private final ByteBuffer ttf; // Storage for font data

    private long win; // The identifier of the GLFW window
    private int width, height; // The pixel dimensions of the GLFW window
    private int display_width, display_height; // The pixel dimensions of the content inside the window, this will usually be the same size as the window.

    private NkContext ctx = NkContext.create(); // Create a Nuklear context, it is used everywhere.
    private NkUserFont default_font = NkUserFont.create(); // This is the Nuklear font object used for rendering text.

    private NkBuffer cmds = NkBuffer.create(); // Stores a list of drawing commands that will be passed to OpenGL to render the interface.
    private NkDrawNullTexture null_texture = NkDrawNullTexture.create(); // An empty texture used for drawing.

    /**
     * The following variables are used for OpenGL.
     */
    private int vbo, vao, ebo;
    private int prog;
    private int vert_shdr;
    private int frag_shdr;
    private int uniform_tex;
    private int uniform_proj;

Create a new GLFW window:

GLFWErrorCallback.createPrint().set();
if (!glfwInit()) {
    throw new IllegalStateException("Unable to initialize glfw");
}

glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
if (Platform.get() == Platform.MACOSX) {
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
}
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);

// Set these to the size of the window you want to create
int WINDOW_WIDTH  = 640;
int WINDOW_HEIGHT = 640;

win = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "GLFW Nuklear Demo", NULL, NULL);
if (win == NULL) {
    throw new RuntimeException("Failed to create the GLFW window");
}

glfwMakeContextCurrent(win);

The following code initializes OpenGL and sets up debug messages. Everything after the first line is optional, you don't necessarily need to turn on debugging for OpenGL.

GLCapabilities caps = GL.createCapabilities();
Callback debugProc = GLUtil.setupDebugMessageCallback();

if (caps.OpenGL43) {
    GL43.glDebugMessageControl(GL43.GL_DEBUG_SOURCE_API, GL43.GL_DEBUG_TYPE_OTHER, GL43.GL_DEBUG_SEVERITY_NOTIFICATION, (IntBuffer)null, false);
} else if (caps.GL_KHR_debug) {
    KHRDebug.glDebugMessageControl(
        KHRDebug.GL_DEBUG_SOURCE_API,
        KHRDebug.GL_DEBUG_TYPE_OTHER,
        KHRDebug.GL_DEBUG_SEVERITY_NOTIFICATION,
        (IntBuffer)null,
        false
    );
} else if (caps.GL_ARB_debug_output) {
    glDebugMessageControlARB(GL_DEBUG_SOURCE_API_ARB, GL_DEBUG_TYPE_OTHER_ARB, GL_DEBUG_SEVERITY_LOW_ARB, (IntBuffer)null, false);
}

Create the Nuklear context. This is an important step, it is used everywhere:

NkContext ctx = setupWindow(win);

The following code is the definition of the setupWindow() method that was just used. The bulk of the method is mapping GLFW mouse and keyboard inputs to Nuklear mouse and keyboard inputs.

private NkContext setupWindow(long win) {
        glfwSetScrollCallback(win, (window, xoffset, yoffset) -> {
            try (MemoryStack stack = stackPush()) {
                NkVec2 scroll = NkVec2.mallocStack(stack)
                    .x((float)xoffset)
                    .y((float)yoffset);
                nk_input_scroll(ctx, scroll);
            }
        });
        glfwSetCharCallback(win, (window, codepoint) -> nk_input_unicode(ctx, codepoint));
        glfwSetKeyCallback(win, (window, key, scancode, action, mods) -> {
            boolean press = action == GLFW_PRESS;
            switch (key) {
                case GLFW_KEY_ESCAPE:
                    glfwSetWindowShouldClose(window, true);
                    break;
                case GLFW_KEY_DELETE:
                    nk_input_key(ctx, NK_KEY_DEL, press);
                    break;
                case GLFW_KEY_ENTER:
                    nk_input_key(ctx, NK_KEY_ENTER, press);
                    break;
                case GLFW_KEY_TAB:
                    nk_input_key(ctx, NK_KEY_TAB, press);
                    break;
                case GLFW_KEY_BACKSPACE:
                    nk_input_key(ctx, NK_KEY_BACKSPACE, press);
                    break;
                case GLFW_KEY_UP:
                    nk_input_key(ctx, NK_KEY_UP, press);
                    break;
                case GLFW_KEY_DOWN:
                    nk_input_key(ctx, NK_KEY_DOWN, press);
                    break;
                case GLFW_KEY_HOME:
                    nk_input_key(ctx, NK_KEY_TEXT_START, press);
                    nk_input_key(ctx, NK_KEY_SCROLL_START, press);
                    break;
                case GLFW_KEY_END:
                    nk_input_key(ctx, NK_KEY_TEXT_END, press);
                    nk_input_key(ctx, NK_KEY_SCROLL_END, press);
                    break;
                case GLFW_KEY_PAGE_DOWN:
                    nk_input_key(ctx, NK_KEY_SCROLL_DOWN, press);
                    break;
                case GLFW_KEY_PAGE_UP:
                    nk_input_key(ctx, NK_KEY_SCROLL_UP, press);
                    break;
                case GLFW_KEY_LEFT_SHIFT:
                case GLFW_KEY_RIGHT_SHIFT:
                    nk_input_key(ctx, NK_KEY_SHIFT, press);
                    break;
                case GLFW_KEY_LEFT_CONTROL:
                case GLFW_KEY_RIGHT_CONTROL:
                    if (press) {
                        nk_input_key(ctx, NK_KEY_COPY, glfwGetKey(window, GLFW_KEY_C) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_PASTE, glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_CUT, glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_UNDO, glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_REDO, glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_WORD_LEFT, glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_WORD_RIGHT, glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_LINE_START, glfwGetKey(window, GLFW_KEY_B) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_TEXT_LINE_END, glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS);
                    } else {
                        nk_input_key(ctx, NK_KEY_LEFT, glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_RIGHT, glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS);
                        nk_input_key(ctx, NK_KEY_COPY, false);
                        nk_input_key(ctx, NK_KEY_PASTE, false);
                        nk_input_key(ctx, NK_KEY_CUT, false);
                        nk_input_key(ctx, NK_KEY_SHIFT, false);
                    }
                    break;
            }
        });
        glfwSetCursorPosCallback(win, (window, xpos, ypos) -> nk_input_motion(ctx, (int)xpos, (int)ypos));
        glfwSetMouseButtonCallback(win, (window, button, action, mods) -> {
            try (MemoryStack stack = stackPush()) {
                DoubleBuffer cx = stack.mallocDouble(1);
                DoubleBuffer cy = stack.mallocDouble(1);

                glfwGetCursorPos(window, cx, cy);

                int x = (int)cx.get(0);
                int y = (int)cy.get(0);

                int nkButton;
                switch (button) {
                    case GLFW_MOUSE_BUTTON_RIGHT:
                        nkButton = NK_BUTTON_RIGHT;
                        break;
                    case GLFW_MOUSE_BUTTON_MIDDLE:
                        nkButton = NK_BUTTON_MIDDLE;
                        break;
                    default:
                        nkButton = NK_BUTTON_LEFT;
                }
                nk_input_button(ctx, nkButton, x, y, action == GLFW_PRESS);
            }
        });

        nk_init(ctx, ALLOCATOR, null);
        ctx.clip(it -> it
            .copy((handle, text, len) -> {
                if (len == 0) {
                    return;
                }

                try (MemoryStack stack = stackPush()) {
                    ByteBuffer str = stack.malloc(len + 1);
                    memCopy(text, memAddress(str), len);
                    str.put(len, (byte)0);

                    glfwSetClipboardString(win, str);
                }
            })
            .paste((handle, edit) -> {
                long text = nglfwGetClipboardString(win);
                if (text != NULL) {
                    nnk_textedit_paste(edit, text, nnk_strlen(text));
                }
            }));

        setupContext();
        return ctx;
}

The above function makes a call to setupContext(). This method is what binds Nuklear to OpenGL for rendering:

private void setupContext() {
        String NK_SHADER_VERSION = Platform.get() == Platform.MACOSX ? "#version 150\n" : "#version 300 es\n";
        String vertex_shader =
            NK_SHADER_VERSION +
            "uniform mat4 ProjMtx;\n" +
            "in vec2 Position;\n" +
            "in vec2 TexCoord;\n" +
            "in vec4 Color;\n" +
            "out vec2 Frag_UV;\n" +
            "out vec4 Frag_Color;\n" +
            "void main() {\n" +
            "   Frag_UV = TexCoord;\n" +
            "   Frag_Color = Color;\n" +
            "   gl_Position = ProjMtx * vec4(Position.xy, 0, 1);\n" +
            "}\n";
        String fragment_shader =
            NK_SHADER_VERSION +
            "precision mediump float;\n" +
            "uniform sampler2D Texture;\n" +
            "in vec2 Frag_UV;\n" +
            "in vec4 Frag_Color;\n" +
            "out vec4 Out_Color;\n" +
            "void main(){\n" +
            "   Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" +
            "}\n";

        nk_buffer_init(cmds, ALLOCATOR, BUFFER_INITIAL_SIZE);
        prog = glCreateProgram();
        vert_shdr = glCreateShader(GL_VERTEX_SHADER);
        frag_shdr = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(vert_shdr, vertex_shader);
        glShaderSource(frag_shdr, fragment_shader);
        glCompileShader(vert_shdr);
        glCompileShader(frag_shdr);
        if (glGetShaderi(vert_shdr, GL_COMPILE_STATUS) != GL_TRUE) {
            throw new IllegalStateException();
        }
        if (glGetShaderi(frag_shdr, GL_COMPILE_STATUS) != GL_TRUE) {
            throw new IllegalStateException();
        }
        glAttachShader(prog, vert_shdr);
        glAttachShader(prog, frag_shdr);
        glLinkProgram(prog);
        if (glGetProgrami(prog, GL_LINK_STATUS) != GL_TRUE) {
            throw new IllegalStateException();
        }

        uniform_tex = glGetUniformLocation(prog, "Texture");
        uniform_proj = glGetUniformLocation(prog, "ProjMtx");
        int attrib_pos = glGetAttribLocation(prog, "Position");
        int attrib_uv  = glGetAttribLocation(prog, "TexCoord");
        int attrib_col = glGetAttribLocation(prog, "Color");

        {
            // buffer setup
            vbo = glGenBuffers();
            ebo = glGenBuffers();
            vao = glGenVertexArrays();

            glBindVertexArray(vao);
            glBindBuffer(GL_ARRAY_BUFFER, vbo);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);

            glEnableVertexAttribArray(attrib_pos);
            glEnableVertexAttribArray(attrib_uv);
            glEnableVertexAttribArray(attrib_col);

            glVertexAttribPointer(attrib_pos, 2, GL_FLOAT, false, 20, 0);
            glVertexAttribPointer(attrib_uv, 2, GL_FLOAT, false, 20, 8);
            glVertexAttribPointer(attrib_col, 4, GL_UNSIGNED_BYTE, true, 20, 16);
        }

        {
            // null texture setup
            int nullTexID = glGenTextures();

            null_texture.texture().id(nullTexID);
            null_texture.uv().set(0.5f, 0.5f);

            glBindTexture(GL_TEXTURE_2D, nullTexID);
            try (MemoryStack stack = stackPush()) {
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, stack.ints(0xFFFFFFFF));
            }
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        }

        glBindTexture(GL_TEXTURE_2D, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
}

The following section creates the font that Nuklear will be using. This section uses the stb library that is packaged with LWJGL.

int BITMAP_W = 1024;
int BITMAP_H = 1024;

int FONT_HEIGHT = 18;
int fontTexID   = glGenTextures();

STBTTFontinfo          fontInfo = STBTTFontinfo.create();
STBTTPackedchar.Buffer cdata    = STBTTPackedchar.create(95);

float scale;
float descent;

try (MemoryStack stack = stackPush()) {
    stbtt_InitFont(fontInfo, ttf);
    scale = stbtt_ScaleForPixelHeight(fontInfo, FONT_HEIGHT);

    IntBuffer d = stack.mallocInt(1);
    stbtt_GetFontVMetrics(fontInfo, null, d, null);
    descent = d.get(0) * scale;

    ByteBuffer bitmap = memAlloc(BITMAP_W * BITMAP_H);

    STBTTPackContext pc = STBTTPackContext.mallocStack(stack);
    stbtt_PackBegin(pc, bitmap, BITMAP_W, BITMAP_H, 0, 1, NULL);
    stbtt_PackSetOversampling(pc, 4, 4);
    stbtt_PackFontRange(pc, ttf, 0, FONT_HEIGHT, 32, cdata);
    stbtt_PackEnd(pc);

    // Convert R8 to RGBA8
    ByteBuffer texture = memAlloc(BITMAP_W * BITMAP_H * 4);
    for (int i = 0; i < bitmap.capacity(); i++) {
        texture.putInt((bitmap.get(i) << 24) | 0x00FFFFFF);
    }
    texture.flip();

    glBindTexture(GL_TEXTURE_2D, fontTexID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, BITMAP_W, BITMAP_H, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    memFree(texture);
    memFree(bitmap);
}

default_font
    .width((handle, h, text, len) -> {
        float text_width = 0;
        try (MemoryStack stack = stackPush()) {
            IntBuffer unicode = stack.mallocInt(1);

            int glyph_len = nnk_utf_decode(text, memAddress(unicode), len);
            int text_len  = glyph_len;

            if (glyph_len == 0) {
                return 0;
            }

            IntBuffer advance = stack.mallocInt(1);
            while (text_len <= len && glyph_len != 0) {
                if (unicode.get(0) == NK_UTF_INVALID) {
                    break;
                }

                /* query currently drawn glyph information */
                stbtt_GetCodepointHMetrics(fontInfo, unicode.get(0), advance, null);
                text_width += advance.get(0) * scale;

                /* offset next glyph */
                glyph_len = nnk_utf_decode(text + text_len, memAddress(unicode), len - text_len);
                text_len += glyph_len;
            }
        }
        return text_width;
    })
    .height(FONT_HEIGHT)
    .query((handle, font_height, glyph, codepoint, next_codepoint) -> {
        try (MemoryStack stack = stackPush()) {
            FloatBuffer x = stack.floats(0.0f);
            FloatBuffer y = stack.floats(0.0f);

            STBTTAlignedQuad q       = STBTTAlignedQuad.mallocStack(stack);
            IntBuffer        advance = stack.mallocInt(1);

            stbtt_GetPackedQuad(cdata, BITMAP_W, BITMAP_H, codepoint - 32, x, y, q, false);
            stbtt_GetCodepointHMetrics(fontInfo, codepoint, advance, null);

            NkUserFontGlyph ufg = NkUserFontGlyph.create(glyph);

            ufg.width(q.x1() - q.x0());
            ufg.height(q.y1() - q.y0());
            ufg.offset().set(q.x0(), q.y0() + (FONT_HEIGHT + descent));
            ufg.xadvance(advance.get(0) * scale);
            ufg.uv(0).set(q.s0(), q.t0());
            ufg.uv(1).set(q.s1(), q.t1());
        }
    })
    .texture(it -> it
        .id(fontTexID));

nk_style_set_font(ctx, default_font);

The final section of the code is your program loop, all of your main software logic is handled in here. All of the Nuklear code described in the rest of this document will go in the block labeled /* Nuklear function calls */.

glfwShowWindow(win);
        while (!glfwWindowShouldClose(win)) {
            /* Determine the size of our GLFW window */
            try (MemoryStack stack = stackPush()) {
                IntBuffer w = stack.mallocInt(1);
                IntBuffer h = stack.mallocInt(1);

                glfwGetWindowSize(win, w, h);
                width = w.get(0);
                height = h.get(0);

                glfwGetFramebufferSize(win, w, h);
                display_width = w.get(0);
                display_height = h.get(0);
            }

            /* Listen for mouse events */
            nk_input_begin(ctx);
            glfwPollEvents();

            NkMouse mouse = ctx.input().mouse();
            if (mouse.grab()) {
                glfwSetInputMode(win, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
            } else if (mouse.grabbed()) {
                float prevX = mouse.prev().x();
                float prevY = mouse.prev().y();
                glfwSetCursorPos(win, prevX, prevY);
                mouse.pos().x(prevX);
                mouse.pos().y(prevY);
            } else if (mouse.ungrab()) {
                glfwSetInputMode(win, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
            }

            nk_input_end(ctx);
            
            /* End listening for mouse events */

            /* Nuklear function calls */
            //
            // Everything mentioned in this guide will go here.
            // I recommend putting all of that code into a method.
            //
            /* End Nuklear function calls */

            /* The following code draws the result to the screen */
            try (MemoryStack stack = stackPush()) {
                IntBuffer width  = stack.mallocInt(1);
                IntBuffer height = stack.mallocInt(1);

                glfwGetWindowSize(win, width, height);
                glViewport(0, 0, width.get(0), height.get(0));

            }
            glClear(GL_COLOR_BUFFER_BIT);
            /*
             * IMPORTANT: `nk_glfw_render` modifies some global OpenGL state
             * with blending, scissor, face culling, depth test and viewport and
             * defaults everything back into a default state.
             * Make sure to either a.) save and restore or b.) reset your own state after
             * rendering the UI.
             */
            render(NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER, MAX_ELEMENT_BUFFER);
            glfwSwapBuffers(win);
        }

        shutdown();

        glfwFreeCallbacks(win);
        if (debugProc != null) {
            debugProc.free();
        }
        glfwTerminate();
Objects.requireNonNull(glfwSetErrorCallback(null)).free();

The above code uses a method called render(). It goes through all of the drawing commands generated by Nuklear and sends them to OpenGL. The code for the render() method is as follows:

private void render(int AA, int max_vertex_buffer, int max_element_buffer) {
    try (MemoryStack stack = stackPush()) {
        // setup global state
        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glDisable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_SCISSOR_TEST);
        glActiveTexture(GL_TEXTURE0);

        // setup program
        glUseProgram(prog);
        glUniform1i(uniform_tex, 0);
        glUniformMatrix4fv(uniform_proj, false, stack.floats(
            2.0f / width, 0.0f, 0.0f, 0.0f,
            0.0f, -2.0f / height, 0.0f, 0.0f,
            0.0f, 0.0f, -1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f, 1.0f
        ));
        glViewport(0, 0, display_width, display_height);
    }

    {
        // convert from command queue into draw list and draw to screen

        // allocate vertex and element buffer
        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);

        glBufferData(GL_ARRAY_BUFFER, max_vertex_buffer, GL_STREAM_DRAW);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, max_element_buffer, GL_STREAM_DRAW);

        // load draw vertices & elements directly into vertex + element buffer
        ByteBuffer vertices = Objects.requireNonNull(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, max_vertex_buffer, null));
        ByteBuffer elements = Objects.requireNonNull(glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY, max_element_buffer, null));
        try (MemoryStack stack = stackPush()) {
            // fill convert configuration
            NkConvertConfig config = NkConvertConfig.callocStack(stack)
                .vertex_layout(VERTEX_LAYOUT)
                .vertex_size(20)
                .vertex_alignment(4)
                .null_texture(null_texture)
                .circle_segment_count(22)
                .curve_segment_count(22)
                .arc_segment_count(22)
                .global_alpha(1.0f)
                .shape_AA(AA)
                .line_AA(AA);

            // setup buffers to load vertices and elements
            NkBuffer vbuf = NkBuffer.mallocStack(stack);
            NkBuffer ebuf = NkBuffer.mallocStack(stack);

            nk_buffer_init_fixed(vbuf, vertices/*, max_vertex_buffer*/);
            nk_buffer_init_fixed(ebuf, elements/*, max_element_buffer*/);
            nk_convert(ctx, cmds, vbuf, ebuf, config);
        }
        glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
        glUnmapBuffer(GL_ARRAY_BUFFER);

        // iterate over and execute each draw command
        float fb_scale_x = (float)display_width / (float)width;
        float fb_scale_y = (float)display_height / (float)height;

        long offset = NULL;
        for (NkDrawCommand cmd = nk__draw_begin(ctx, cmds); cmd != null; cmd = nk__draw_next(cmd, cmds, ctx)) {
            if (cmd.elem_count() == 0) {
                continue;
            }
            glBindTexture(GL_TEXTURE_2D, cmd.texture().id());
            glScissor(
                (int)(cmd.clip_rect().x() * fb_scale_x),
                (int)((height - (int)(cmd.clip_rect().y() + cmd.clip_rect().h())) * fb_scale_y),
                (int)(cmd.clip_rect().w() * fb_scale_x),
                (int)(cmd.clip_rect().h() * fb_scale_y)
            );
            glDrawElements(GL_TRIANGLES, cmd.elem_count(), GL_UNSIGNED_SHORT, offset);
            offset += cmd.elem_count() * 2;
        }
        nk_clear(ctx);
    }

    // default OpenGL state
    glUseProgram(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glDisable(GL_BLEND);
    glDisable(GL_SCISSOR_TEST);
}

That's quite a lot of code, if you have any questions feel free to send me an e-mail.

Layout Reference: Windows, Rows and Groups

Nuklear provides UI widgets such as sliders, text fields and buttons, but also provides a system to organize and align them. At the root are windows, these have a specified position and size on the screen. Inside each window you can place rows, these rows are what contain the widgets. Inside a row you may place a group. A group is a container that contains its own set of rows with widgets in them. These will be explained in detail in the sub-sections below.

A diagram outlining the different layout components of Nuklear
A diagram outlining the different layout components of Nuklear.

Window

The root object in any Nuklear layout is the window. The window is not contained by anything, but there can be more than one. When the content of a window does not fit in the window either horizontally or vertically, scroll bars will appear automatically.

Creating a window

All windows begin with a call to nk_begin() and end with a call to nk_end(), everything in between the two calls is contained in the window. If the nk_begin() call returns false it is an indication that the content of the window should not be drawn (e.g. the window is minimized). For that reason, the content of the window should be wrapped in an if() statement.

Since the name of the window has to be unique, the nk_begin_titled() function was created so that the title bar could show different text than the window's name, allowing two different windows to have the same title while having different names.

// Read about the MemoryStack in the struct reference.
try(MemoryStack stack = MemoryStack.stackPush()) {
    // Create a rectangle for the window
    NkRect rect = NkRect.mallocStack(stack);
    rect.x(50).y(50).w(300).h(200);
    // Begin the window
    if(nk_begin(context, "Window Name", rect, NK_WINDOW_TITLE|NK_WINDOW_BORDER|NK_WINDOW_MINIMIZABLE)) {
        // Add rows here
        float rowHeight = 50;
        int itemsPerRow = 1;
        nk_layout_row_dynamic(context, rowHeight, itemsPerRow);
    }
    // End the window
    nk_end(ctx);
}

The nk_end() function call must be outside of the if() condition, otherwise it will not work. On the other hand, the Group widget actually needs to have its _end() function inside of the if statement.

Making a Closable Window

Nuklear does not close windows for you. In order to close a window you have to check that the close button was pressed. This is done by checking the nk_window_is_closed() function.

Now, because the window is considered closed if it has not yet been drawn, the following code will never actually draw the window.

if(!nk_window_is_closed(context, "My Window")) {
    try(MemoryStack stack = MemoryStack.stackBuffer()) {
        NkRect rect = NkRect.mallocStack(stack);
        nk_rect(10, 10, 300, 200, rect);
        if(nk_begin(context, "My Window", rect, NK_WINDOW_TITLE|NK_WINDOW_BORDER|NK_WINDOW_MINIMIZABLE|NK_WINDOW_CLOSABLE)) {
            // Widgets in here
        }
        nk_end(context);
    }
}

To solve this, we must have at least some code outside of the program loop. It will either be used to detect if the window has been drawn yet or just draw the window before the loop starts.

For efficiency, I will create a property that can both check if the window has been drawn and also keep the window's NkRect object so that we don't have to create a new one on each loop. With this approach, we won't need the MemoryStack any more. While we're at it, let's store other window attributes that never change in their own properties.

private NkRect windowRect = null;
private String windowName = "My Window";
private int windowOptions = NK_WINDOW_TITLE|NK_WINDOW_BORDER|NK_WINDOW_MINIMIZABLE|NK_WINDOW_CLOSABLE;
if(windowRect == null) {
    // Create the rectangle that represents
    windowRect = NkRect.create();
    nk_rect(10, 10, 300, 200, rect);

    // Draw the window just once to create it
    nk_begin(context, windowName, rect, windowOptions);
    nk_end(context);
} else if(!nk_window_is_closed(context, windowName)) {
    if(nk_begin(context, windowName, rect, windowOptions)) {
        // Widgets in here
    }
    nk_end(context);
}

You might want to abstract this window code into its own class that lets you set its options and run the rendering code and you could also add a feature that lets you re-open the same window later on.

Creating a Sidebar

By sidebar, I mean a static rectangle on the screen that just contains UI widgets. It has no header, or borders and optionally no background. This technique just uses the window options described in the nk_begin() reference.

Let's assume we just want a left sidebar on the screen. To make a sidebar we just create a window and not set any of the options (though I recommend having a scrollbar if you expect the widgets to not fit in the window.)

    // Create a property for the sidebar's rectangle
    // This sidebar is attached to the left of the screen, is 200 pixels wide and 600 pixels tall.
    private NkRect sidebarRect = NkRect.create().x(0).y(0).w(200).h(600);
// Setting the options to 0 means no options
if(nk_begin(context, "Sidebar", sidebarRect, 0)) {
    // Code for widgets here
}
nk_end(context);

This is cool, but the sidebar isn't the full height of the screen and we can't align it to the right. To fix this, I have created a small abstraction. The abstraction recalculates the window's rectangle on each program loop. Due to the nature of Nuklear windows, this technique does not work on windows that can be resized by the user because Nuklear ignores any size changes after the window is first created.

public class Sidebar {
    private final NkContext context;
    private final String name;
    private final NkRect rect;
    private boolean alignToRight;
    public Sidebar(NkContext context, String name, int width, boolean alignToRight) {
        this.context = context;
        this.name = name;
        this.alignToRight = alignToRight;
        rect = NkRect.create().x(0).y(0).w(width);
    }
    public void begin(int screenWidth, int screenHeight) {
        // Set the height of the rectangle to the height of the screen
        rect.h(screenHeight);
        // If we want to align it to the right, set the X coordinate to the
        // screen width minus the width of the sidebar
        if(alignToRight) {
            rect.x(screenWidth - width);
        }
        // Begin drawing the sidebar
        nk_begin(context, name, rect, windowOptions);
    }
    public void end() {
        // Finish drawing the sidebar
        nk_end(context);
    }
}

The way we use the above class is as follows:

  1. Instantiate the class outside of the loop.
  2. Call the begin() method.
  3. Draw all the widgets that belong to the sidebar.
  4. Call the end() method.

I am going to include part of our program loop to illustrate the following example. The full code is in the setup section of this document.

// Instantiate the sidebar
// This sidebar is 200 pixels wide and attached to the right side of the screen
Sidebar sidebar = new Sidebar(context, "Sidebar", 200, true);

while (!glfwWindowShouldClose(win)) {
    /* Determine the size of our GLFW window */
    try (MemoryStack stack = stackPush()) {
        IntBuffer w = stack.mallocInt(1);
        IntBuffer h = stack.mallocInt(1);

        glfwGetWindowSize(win, w, h);
        width = w.get(0);
        height = h.get(0);

        glfwGetFramebufferSize(win, w, h);
        display_width = w.get(0);
        display_height = h.get(0);
    }

    //
    // ...
    //

    /* Nuklear function calls */
    
    sidebar.begin(display_width, display_height);
    //
    // Code for widgets inside here
    //
    sidebar.end();
    
    /* End Nuklear function calls */

    //
    // ...
    //
    
    glfwSwapBuffers(win);
}

The same technique I used on the sidebar can be used to make all kinds of window alignments and flexible sizes. If you want to make a window that is half the width of the screen then set the width of the rectangle to half of the screen width. Here are a few examples of what to put in the render() method to get certain results:

// 50% wide, 100% height, 25% from the left (centered)
rect.x(0.25f * screenWidth).y(0).w(0.5f * screenWidth).h(screenHeight);

// Bottom bar
int height = 100;
rect.x(0).y(screenHeight - height).w(screenWidth).h(height);

// 10% from the right, 10% from the top, 200px wide and 200px tall
float x = screenWidth - 0.1f * screenWidth;  // Same as "x = 0.9f * screenWidth"
float y = 0.1f * screenHeight;
rect.x(x).y(y).w(200).height(200);

Styling the Window

See the styling guide before reading this section.

The following code shows a variety of different ways to style the window. Remember that if you are creating structs within the loop you need to use a MemoryStack to correctly handle the memory they use. Images need a texture ID provided by OpenGL.

Padding and borders are fitted within the rectangle of the window so that the window remains the same width with or without borders and padding.

Note: For some weird reason, padding on the window and groups is doubled horizontally. Setting a horizontal padding of 4 pixels will result in 8 pixels of space between the edge of the window and the content. This does not occur for vertical padding.

/* Structs and resources used for examples */
// OpenGL texture code
int textureID = glGenTextures();
//
// OpenGL texture loading code
//
NkColor blue = NkColor.create().set((byte)0x00, (byte)0x00, (byte)0xFF, (byte)0xFF);
NkColor red = NkColor.create().set((byte)0xFF, (byte)0x00, (byte)0x00, (byte)0xFF);
NkColor white = NkColor.create().set((byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF);
NkImage image = NkImage.create();
image.handle(it -> it.id(textureID)); // See NkImage for details

/* Change the background color */
// The background color is an NkColor struct.
// The window style actually has a .background() property, but it does not
// appear to work. The .fixed_background() property is what needs to be set.
context.style().window().fixed_background().data().color().set(white); // Passing in a struct
context.style().window().fixed_background().data().color().set((byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF); // Passing in color values

/* Set a background image */
context.style().window().fixed_background().data().image(image); // Pass in an NkImage struct

/* Rounded corners */
// Set the radius of the rounded corners
context.style().window().rounding(5);

/* Border and padding */
float xPadding = 20;
float yPadding = 10;
context.style().window().border(2); // Can be a floating point value
context.style().window().border_color(blue);
context.style().window().padding().set(xPadding, yPadding);

// Read padding values
NkVec2 pad = context.style().window().padding();
float xPad = pad.x();
float yPad = pad.y();

/* Set the title bar color */
// When not focused
context.style().window().header().normal().data().color().set(blue); // Passing in a struct
context.style().window().header().normal().data().color().set(byte r, byte g, byte b, byte a); // Passing in color values

// When focused (if not set, uses the normal() value)
context.style().window().header().active().data().color().set(blue); // Passing in a struct
context.style().window().header().active().data().color().set(byte r, byte g, byte b, byte a); // Passing in color values

// When hovering (if not set, uses active() when focused and normal() otherwise)
context.style().window().header().hover().data().color().set(blue); // Passing in a struct
context.style().window().header().hover().data().color().set(byte r, byte g, byte b, byte a); // Passing in color values

/* Set the title bar background image */
context.style().window().header().normal().data().image(image); // Pass in an NkImage struct
context.style().window().header().active().data().image(image); // Pass in an NkImage struct
context.style().window().header().hover().data().image(image); // Pass in an NkImage struct

/* Title bar text */
// All NkStyleWindowHeader properties are documented right here
NkStyleWindowHeader titleBar = context.style().window().header();

// Padding
NkVec2 padding = titleBar.label_padding();
padding.x(10);
padding.y(10);

// Text color
// label_normal() is used for unfocused windows.
// label_active() is used for focused windows.
// label_hover() is used when the mouse is over the title bar.
titleBar.label_normal().set(white); // Passing in an NkColor struct
titleBar.label_active().set(red);   // Passing in an NkColor struct
titleBar.label_hover().set((byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xFF); // Passing in color values

/* Set header button symbols */
/* Pass in one of the following constants:
 * NK_SYMBOL_CIRCLE_OUTLINE
 * NK_SYMBOL_CIRCLE_SOLID
 * NK_SYMBOL_MAX
 * NK_SYMBOL_MINUS
 * NK_SYMBOL_NONE
 * NK_SYMBOL_PLUS
 * NK_SYMBOL_RECT_OUTLINE
 * NK_SYMBOL_RECT_SOLID
 * NK_SYMBOL_TRIANGLE_DOWN
 * NK_SYMBOL_TRIANGLE_LEFT
 * NK_SYMBOL_TRIANGLE_RIGHT
 * NK_SYMBOL_TRIANGLE_UP
 * NK_SYMBOL_UNDERSCORE
 * NK_SYMBOL_X
 */
NkStyleWindowHeader header = context.style().window().header();
header.close_symbol(NK_SYMBOL_X);
header.maximize_symbol(NK_SYMBOL_RECT_OUTLINE);
header.minimize_symbol(NK_SYMBOL_UNDERSCORE);

/* Header button colors and images */
// These objects return an NkStyleButton. All of the NkStyleButton properties
// are documented right here.
NkStyleButton style;

// Style the close window button
style = context.style().window().header().close_button();
style.normal().data().color().set(blue);
style.normal().data().image().set(image);
style.active().data().color().set(blue);
style.active().data().image().set(image);
style.hover().data().color().set(blue);
style.hover().data().image().set(image);

// Style the minimize button
style = context.style().window().header().minimize_button();
style.normal().data().color().set(blue);
style.normal().data().image().set(image);
style.active().data().color().set(blue);
style.active().data().image().set(image);
style.hover().data().color().set(blue);
style.hover().data().image().set(image);

An Important Note on Scroll Bars

This doesn't really fit nicely in any of this guide's categories, but since windows have scroll bars I may as well put it here. There either is an issue with scroll bars in Nuklear, or the way to prevent the behaviour I am about to describe is just not documented anywhere.

As long as a window or group is capable of having a scroll bar, the space for that scroll bar is reserved. This would not be a problem if you could see a blank scroll bar in the reserved space, but there is no scroll bar, it just acts like a large amount of padding on the right side and bottom of the window. The only way to get rid of this space is to disable scroll bars on the window, but I don't want them disabled. I would be content with any of the following behaviours:

  1. The scroll bar is always visible even if no scrolling is necessary.
  2. The scroll bar is gone and does not reserve any space if scrolling is not needed.

The way Nuklear has implemented the scroll bars is the worst combination of hiding and reserving space.

I tried adding the NK_WINDOW_SCROLL_AUTO_HIDE option to the window, but it does not do anything. I suspect that, if the window needs scrollbars, this option will hide them after a few seconds of inactivity but still keep the space reserved, which is still the same problem.

Row

A row reserves a section of horizontal space in a window and contains widgets, in fact all widgets must be contained directly by a row. The first row starts at the top of the window or group that contains it and each subsequent row is placed below the one before it. You must specify the height of the row and the number of widgets that can fit next to each other on each line.

There are a variety of different row functions. A row implicitly ends when another row begins or when the window or group that it belongs to ends.

Each row declaration actually adds as many rows as are needed to hold all the widgets. If you declare a row that allows three items per row and then put four widgets in it, two rows will appear.

There are two types of row: Static rows and dynamic rows. I will start off describing the dynamic row because I find it more useful.

Dynamic Rows

In a dynamic row, each widget in the row occupies a width specified as a fraction of the available horizontal space. By default the widgets are spaced equally, but it is possible to specify the fraction of space for each widget.

To create a dynamic row inside a widget or group, you can use the nk_layout_row_dynamic() function to equally space out the widgets or nk_layout_row_begin() to choose how much space each widget takes. If nk_layout_row_begin() is used then nk_layout_row_end() must be called after all the widgets have been added. When using nk_layout_row_begin(), to select the width of the widget call the nk_layout_row_push() function.

/* A dynamic row with three widgets of equal width */
float height = 30; // The row is 30 pixels tall
int itemsPerRow = 3; // How many widgets can go in one row
nk_layout_row_dynamic(context, height, itemsPerRow);
// Add widgets here
//

/* A dynamic row where the width for each row is specified for each widget */
nk_layout_row_begin(context, NK_STATIC, height, itemsPerRow);

nk_layout_row_push(context, 0.4f); // 40% wide
// First widget here
//

nk_layout_row_push(context, 0.4f); // 40% wide
// Second widget here
//

nk_layout_row_push(context, 0.2f); // 20% wide
// Third widget here
//

// You must explicitly declare that the row ended
nk_layout_row_end(context);

Static Rows

In a static row, each widget in the row occupies a fixed width specified in pixels. The sizes of these widgets does not depend on the size of the container.

To create a static row, use the nk_layout_row_static() function to specify the width of all widgets at the same time or use nk_layout_row_begin() to give each widget its own width. If nk_layout_row_begin() is used then nk_layout_row_end() must be called after all the widgets have been added. When using nk_layout_row_begin(), to select the width of the widget call the nk_layout_row_push() function.

/* A static row with three widgets of equal width */
float height = 30; // The row is 30 pixels tall
float width = 80; // Each widget is 80 pixels wide
int itemsPerRow = 3; // How many widgets can go in one row
nk_layout_row_static(context, height, width, itemsPerRow);
// Add widgets here
//

/* A static row where the width for each row is specified for each widget */
nk_layout_row_begin(context, NK_DYNAMIC, height, itemsPerRow);

nk_layout_row_push(context, 80);
// First widget here
//

nk_layout_row_push(context, 80);
// Second widget here
//

nk_layout_row_push(context, 40); // 20% wide
// Third widget here
//

// You must explicitly declare that the row ended
nk_layout_row_end(context);

Styling Rows

See the styling guide before reading this section.

The only style that could really be associated with rows is spacing. The spacing determines how much space exists between widgets in the row as well as between rows.

/* Spacing is a property of the window styling */
// Set all spacing at once
int horizontal = 2;
int vertical = 4;
context.style().window().spacing().set(horizontal, vertical);

// Set individual values
context.style().window().spacing().x(horizontal);
context.style().window().spacing().y(vertical);

// Read values
float currentHSpace = context.style().window().spacing().x();
float currentVSpace = context.style().window().spacing().y();

Group

A group is a container for widgets. It must be embedded in a row like a widget, but contains its own rows and (optionally) has background, borders, title bar and scrollbars like a window. Groups even borrow a lot of their styling directly from the window styling properties. See the styling guide to learn how to give a group a different style from a window using the same properties.

Like the window, the group has its own _begin() and _end() functions and the group's name is a unique ID for the group. Due to the name being a unique ID, as with the window, the group has a _begin_titled() function as well to allow for more than one group with the same text in the title bar.

The height of a group is given by the height of the row that contains it. If the widgets inside the group do not fit within that height then a scroll bar will appear.


// All groups must be contained in a row
nk_layout_row_dynamic(context, 100, 1); // The row is 100 pixels tall and at most 1 element fits in a row

// Groups have the same options available as windows
int options = NK_WINDOW_BORDER | NK_WINDOW_NO_SCROLLBAR;
if(nk_group_begin(context, "My Group", options)) {

  //
  // The group contains rows and the rows contain widgets, put those here.
  //
  
  // Unlike the window, the _end() function must be inside the if() block
  nk_group_end(context);
}

Styling Groups

See the styling guide before reading this section.

Groups inherit their background color and all title bar styles from the window styles. For information on these properties, see styling the window. The group's border and padding are configured separately from the window. As with the window styles, the horizontal padding is always double of what was specified in the styling rule.

// Set the width of the border
context.style().window().group_border(3);

// Set the color of the group's border
NkColor color = context.style().window().group_border_color();
color.r((byte)0x00).g((byte)0xA0).b((byte)0x00).a((byte)0xFF);

// Set the padding of the group
NkVec2 padding = context.style().window().group_padding();
padding.x(2);
padding.y(4);

Styling Guide

Styles for all widgets are in the .style() struct of the context. All styling rules are in the form of structs and should be treated that way. To see a description of the styling rules for all of the widgets, go to the section that describes the widget or layout element that the style applies to. This section of the document only describes how styles are used in general.

Creating a Theme

To begin, we can employ a quick method to initialize all of the colors of Nuklear at once. It uses the nk_style_from_table() function. This function takes in a buffer of structs to set up the colors (a struct buffer is an LWJGL thing that is more efficient than an array of structs). Each element in the buffer has one of the NK_COLOR_* constants as its index, which indicates which type of element is being given the color, you can see the C version of this on the Nuklear Github page.

This is the only code example where struct buffers are used, so I will not create a guide describing how struct buffers work in general; instead I will just show how to use the buffer in the example code.

While this is a good way to quickly set up the basic colors for a theme, many of these colors apply to several different widgets at the same time. If you want to have different colors for each of those widgets then you will have to use the context.style() struct which will be covered in the next section of the styling guide.

And now I'll show you the code to initialize the colors of your theme. I have wrapped it in a function and added a createColor() function for convenience because casting ints to bytes is tedious.

private function initializeTheme(NkContext context) {
    // The MemoryStack is described in the Struct Reference
    try(MemoryStack stack = MemoryStack.stackPush()) {
      
        // The list of colors we want to use
        NkColor white = createColor(stack, 255, 255, 255, 255);
        NkColor black = createColor(stack, 0, 0, 0, 255);
        NkColor grey01 = createColor(stack, 45, 45, 45, 255);
        NkColor grey02 = createColor(stack, 70, 70, 70, 255);
        NkColor grey03 = createColor(stack, 120, 120, 120, 255);
        NkColor grey04 = createColor(stack, 140, 140, 140, 255);
        NkColor grey05 = createColor(stack, 150, 150, 150, 255);
        NkColor grey06 = createColor(stack, 160, 160, 160, 255);
        NkColor grey07 = createColor(stack, 170, 170, 170, 255);
        NkColor grey08 = createColor(stack, 180, 180, 180, 255);
        NkColor grey09 = createColor(stack, 185, 185, 185, 255);
        NkColor grey10 = createColor(stack, 190, 190, 190, 255);
        NkColor grey11 = createColor(stack, 200, 200, 200, 255);
        NkColor grey12 = createColor(stack, 240, 240, 240, 255);
        NkColor blue1 = createColor(stack, 80, 80, 200, 255);
        NkColor blue2 = createColor(stack, 128, 196, 255, 255);
        NkColor blue3 = createColor(stack, 64, 196, 255, 255);
        NkColor red = createColor(stack, 255, 0, 0, 255);
        
        // This buffer acts like an array of NkColor structs
        int size = NkColor.SIZEOF * NK_COLOR_COUNT; // How much memory we need to store all the color data
        ByteBuffer buffer = stack.calloc(size);
        NkColor.Buffer colors = new NkColor.Buffer(buffer);
        colors.put(NK_COLOR_TEXT, black);
        colors.put(NK_COLOR_WINDOW, grey11);
        colors.put(NK_COLOR_HEADER, blue1);
        colors.put(NK_COLOR_BORDER, black);
        colors.put(NK_COLOR_BUTTON, grey09);
        colors.put(NK_COLOR_BUTTON_HOVER, grey07);
        colors.put(NK_COLOR_BUTTON_ACTIVE, grey06);
        colors.put(NK_COLOR_TOGGLE, grey05);
        colors.put(NK_COLOR_TOGGLE_HOVER, grey03);
        colors.put(NK_COLOR_TOGGLE_CURSOR, grey10);
        colors.put(NK_COLOR_SELECT, grey06);
        colors.put(NK_COLOR_SELECT_ACTIVE, white);
        colors.put(NK_COLOR_SLIDER, grey12);
        colors.put(NK_COLOR_SLIDER_CURSOR, blue2);
        colors.put(NK_COLOR_SLIDER_CURSOR_HOVER, blue3);
        colors.put(NK_COLOR_SLIDER_CURSOR_ACTIVE, blue2);
        colors.put(NK_COLOR_PROPERTY, grey10);
        colors.put(NK_COLOR_EDIT, grey05);
        colors.put(NK_COLOR_EDIT_CURSOR, black);
        colors.put(NK_COLOR_COMBO, grey10);
        colors.put(NK_COLOR_CHART, grey06);
        colors.put(NK_COLOR_CHART_COLOR, grey01);
        colors.put(NK_COLOR_CHART_COLOR_HIGHLIGHT, red);
        colors.put(NK_COLOR_SCROLLBAR, grey08);
        colors.put(NK_COLOR_SCROLLBAR_CURSOR, grey04);
        colors.put(NK_COLOR_SCROLLBAR_CURSOR_HOVER, grey05);
        colors.put(NK_COLOR_SCROLLBAR_CURSOR_ACTIVE, grey06);
        colors.put(NK_COLOR_TAB_HEADER, grey08);
        nk_style_from_table(context, colors);
    }
}

public static NkColor createColor(MemoryStack stack, int r, int g, int b, int a) {
    return NkColor.mallocStack(stack).set((byte) r, (byte) g, (byte) b, (byte) a);
}

After setting the colors, you can also set all of the general styles at the same time in one function, similar to the code above. As an example, the following code is part of my theme function:

    context.style().edit().normal().data().color().set(white);
    context.style().edit().hover().data().color().set(white);
    context.style().edit().selected_normal().set(white);
    context.style().edit().selected_hover().set(white);
    context.style().edit().active().data().color().set(white);

    context.style().window().header().padding().set(1, 1);
    context.style().window().header().label_padding().set(1, 1);

    context.style().window().header().label_normal().set(white);
    context.style().window().header().label_hover().set(white);
    context.style().window().header().label_active().set(white);

    context.style().window().spacing().set(0, 0);
    context.style().window().padding().set(2, 4);
    context.style().window().group_padding().set(0, 0);
    context.style().window().group_border_color().set(grey02);

    context.style().window().scrollbar_size().set(16, 16);
    context.style().scrollv().show_buttons(1);
    context.style().scrollv().inc_symbol(NK_SYMBOL_TRIANGLE_DOWN);
    context.style().scrollv().dec_symbol(NK_SYMBOL_TRIANGLE_UP);
    context.style().scrollv().border_cursor(1);
    context.style().scrollv().dec_button().normal().data().color().set(grey09);
    context.style().scrollv().dec_button().text_normal().set(black);
    context.style().scrollv().inc_button().set(context.style().scrollv().dec_button());

    context.style().scrollh().show_buttons(1);
    context.style().scrollh().inc_symbol(NK_SYMBOL_TRIANGLE_RIGHT);
    context.style().scrollh().dec_symbol(NK_SYMBOL_TRIANGLE_LEFT);
    context.style().scrollh().border_cursor(1);
    context.style().scrollh().dec_button().normal().data().color().set(grey09);
    context.style().scrollh().dec_button().text_normal().set(black);
    context.style().scrollh().inc_button().set(context.style().scrollv().dec_button());

    context.style().slider().bar_filled().set(blue2);

About the context.style() Object

Most of the properties in this data structure are structs and there are two different ways to set them:

NkColor red = NkColor.create().set((byte)0xFF, 0, 0, (byte)0xFF);

// Method 1:
context.style.window().fixed_background().data().color(red);

// Method 2:
context.style.window().fixed_background().data().color().set(red);

What is the difference? In technical terms: The first method points the color property to the red struct, while the second one copies the properties of red to the existing struct.

What does that mean? If you use the first method and pass the same color to more than one style then all of the styles will be linked: Changing one of them changes all of them. If you use the second method, every object gets its own independent color. Here is a code example:

NkColor red = NkColor.create().set((byte)0xFF, 0, 0, (byte)0xFF);

// Set the colors
context.style.window().fixed_background().data().color(red); // Window background color
context.style().button().active().data().color(red); // Active button color

// Make the button transparent by setting its alpha
context.style().button().active().data().color().a((byte) 0x80);

/* Since the button color and window background are linked, this code also makes
the window background transparent as well. */

The above code initially sets the window background and button to be red, but then changing the button's color is also changing the window's color. If this is the desired behaviour then use this method, otherwise you should use .set() instead.

Overriding Styles

When we created our theme, we set up some general styles, but some of those style rules apply to several different elements. One example is that context.window().fixed_background().data().color() sets the background color for the window but also sets it for groups. Even if that wasn't the case, sometimes we just want two elements of the same type to have different appearances.

Fortunately, Nuklear's style rules can be set right before the widget is drawn, overriding the value they had before. Take the following example:

byte r = (byte) 0x99;
byte g = (byte) 0xA0;
byte b = (byte) 0xFF;
byte a = (byte) 0xFF;
context.style().window().fixed_background().data().color().set(r, g, b, a);

int options = 0; // No options, just make a rectangle
if(nk_group_begin(context, "My Group", options)) {
  //
  // Rows and widgets here
  //
  nk_group_end(context);
}

The above code will set the background color of the group, but we forgot something! Once a style has been changed, it remains that way, which means that all windows and groups drawn from now on will have the color we just specified. To solve this, we should set the color back to what it was before once we are done using it. Here's how:

try(MemoryStack stack = MemoryStack.stackPush()) {
    // Remember what the color used to be
    NkColor oldColor = NkColor.mallocStack(stack);
    oldColor.set(context.style().window().fixed_background().data().color());

    // Set the new color
    byte r = (byte) 0x99;
    byte g = (byte) 0xA0;
    byte b = (byte) 0xFF;
    byte a = (byte) 0xFF;
    context.style().window().fixed_background().data().color().set(r, g, b, a);

    // Make the group
    int options = 0; // No options, just make a rectangle
    if(nk_group_begin(context, "My Group", options)) {
      //
      // Rows and widgets here
      //
      nk_group_end(context);
    }

    // Set the color back to what it was before
    context.style().window().fixed_background().data().color().set(oldColor);
}

I'm pretty sure that the Nuklear developers would recommend adding the color to a stack instead, so I will explain that. There is a stack for each basic style struct, these are all the functions for adding to a stack:

Each of the stack functions, with the exception of nk_style_push_font() takes in two parameters aside from the context (context is used in practically all Nuklear functions). The first of the two is a pointer to the property that we want to set, such as context.style().window().fixed_background().data().color(), the second is the value we want to set it to in the form of a struct.

A push function adds a new style to the top of the stack, the widgets and layout elements use whichever value is at the top of the stack. When you are done with the style, you should use a pop function to remove the style from the top of the stack so that it returns to the value it had before. Every push function has a corresponding pop function with the same name. The following is a code example that uses the stack instead of remembering old values:

// See the Struct Reference for information about the MemoryStack
try(MemoryStack stack = MemoryStack.stackPush()) {
    // Create the new color
    byte r = (byte) 0x99;
    byte g = (byte) 0xA0;
    byte b = (byte) 0xFF;
    byte a = (byte) 0xFF;
    NkColor newColor = NkColor.mallocStack(stack).set(r, g, b, a)

    // Add the color to the stack
    nk_style_push_color(context, context.style().window().fixed_background().data().color(), newColor);

    // Make the group
    int options = 0; // No options, just make a rectangle
    if(nk_group_begin(context, "My Group", options)) {
      //
      // Rows and widgets here
      //
      nk_group_end(context);
    }

    // Set the color back to what it was before
    nk_style_pop_color(context);
}

The nk_style_push_font() function only takes one parameter in addition to the context because one font is used for the whole Nuklear context. The parameter you pass to it is an NkFont struct like the one we created in the Setting It All Up section.

Now that you know how to use a stack, let me explain some of the problems with the stacks.

The first problem with stacks probably stems from using the LWJGL binding of Nuklear. The stacks rely on references to the properties, but in the case of nk_style_push_float() and nk_style_push_flags(), the properties that we are trying to set are primitives, and so context.style().window().border() is not actually a reference and we can't use the following code to push onto the stack:

// The following code will not work because context.style().window().border()
// is a float, not a FloatBuffer as the function requires.
nk_style_push_float(context, context.style().window().border(), 5);

For that reason, I am not documenting nk_style_push_float() or nk_style_push_flags() and I recommend using the previous technique (storing the old value and setting it back when you're done) for changing float properties and flags.

I have to emphasize that for every single push function you call, you have to call a pop function once you're done. I will show just one more example which handles multiple stacks, this is an excerpt from my own software:

// The variable name "stack" here actually has nothing to do with the style
// stacks we're talking about. Please check the struct reference for more
// information about the MemoryStack.
try(MemoryStack stack = stackPush()) {
    // Set styles
    NkVec2 pad = NkVec2.mallocStack(stack).set(0, 0);
    NkVec2 imgPad = NkVec2.mallocStack(stack).set(2, 2);
    nk_style_push_vec2(context, context.style().selectable().padding(), pad);
    nk_style_push_vec2(context, context.style().selectable().image_padding(), imgPad);
    nk_style_push_color(context, context.style().window().fixed_background().data().color(), backgroundColor);
    nk_style_push_color(context, context.style().selectable().normal().data().color(), normalColor);
    nk_style_push_color(context, context.style().selectable().hover().data().color(), normalColor);
    nk_style_push_color(context, context.style().selectable().pressed().data().color(), normalColor);
    nk_style_push_color(context, context.style().selectable().normal_active().data().color(), selectedColor);
    nk_style_push_color(context, context.style().selectable().hover_active().data().color(), selectedColor);
    nk_style_push_color(context, context.style().selectable().pressed_active().data().color(), selectedColor);

    // Rendering
    nk_layout_row_dynamic(context, 20, 1);
    nk_label(context, "Things", NK_TEXT_ALIGN_LEFT | NK_TEXT_ALIGN_BOTTOM);
    nk_layout_row_dynamic(context, 200, 1);
    if(nk_group_begin(context, "Group1", windowOptions)) {
        nk_layout_row_begin(context, NK_STATIC, 50, 4);
        
        //
        // Several widgets drawn here
        //
        
        nk_layout_row_end(context);
        nk_group_end(context);
    }

    // Restore previous styles
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_color(context);
    nk_style_pop_vec2(context);
    nk_style_pop_vec2(context);
}

As you see, I have seven push_color() calls and seven pop_color() calls, two push_vec2() calls and two pop_vec2() calls.

I think that's it for stacks, if you still have questions, be sure to send me an e-mail.

Widget Reference

For the purpose of handling variables that contain user input from the widgets and for illustrating data that must be initialized outside of the program loop, I will be using functions and classes in the usage examples.

If you come across a function declaration, the function needs to be called in the program loop. If you come across a class, the class instance must be instantiated before the program loop and have its method called inside the loop.

Label

This widget's only purpose is to show some text. It may be used next to another widget to show its name or value. To use it, call the nk_label() function and pass in the string to be shown and some flags to indicate how the text should be aligned.

The constants for aligning text are:

  • NK_TEXT_ALIGN_LEFT - Left aligned
  • NK_TEXT_ALIGN_RIGHT - Right aligned
  • NK_TEXT_ALIGN_CENTERED - Horizontally centered
  • NK_TEXT_ALIGN_TOP - Aligned to the top of the row
  • NK_TEXT_ALIGN_BOTTOM - Aligned to the bottom of the row
  • NK_TEXT_ALIGN_MIDDLE - Vertically centered

Usage examples

// A label on its own
nk_layout_row_dynamic(context, 20, 1);
nk_label(context, "This is the label text", NK_TEXT_ALIGN_LEFT);
/* Using labels to show the name and value of a slider */

// This IntBuffer contains the number that the user selected by using the slider
// and is updated automatically when nk_slider_int is called.
private IntBuffer currentValue = BufferUtils.createIntBuffer(1);

// The showSlider() function should run in every program loop
public void showSlider() {
    // Show the name of the slider
    nk_layout_row_dynamic(context, 32, 1); // 32 pixels tall, one item in the row
    nk_label(context, "Slider", NK_TEXT_ALIGN_LEFT | NK_TEXT_ALIGN_BOTTOM);

    // Create the slider
    int min = 0;
    int max = 20;
    int step = 1;
    nk_layout_row_dynamic(context, Window.FONT_HEIGHT, 1);
    nk_slider_int(context, min, currentValue, max, step);

    // Show numbers for the slider's min, max and current value right below the slider
    nk_layout_row_dynamic(context, 20, 3); // 20 pixels tall, 3 items in the row
    nk_label(context, String.valueOf(min), NK_TEXT_ALIGN_LEFT | NK_TEXT_ALIGN_TOP);
    nk_label(context, String.valueOf(currentValue.get(0)), NK_TEXT_ALIGN_CENTERED | NK_TEXT_ALIGN_TOP);
    nk_label(context, String.valueOf(max), NK_TEXT_ALIGN_RIGHT | NK_TEXT_ALIGN_TOP);
}

Styling

See the styling guide before reading this section.

The following style rules can be applied to a label:

/* Text color */
// Passing in color values directly
context.style().text().color().set((byte) 0xFF, 0, 0, (byte) 0xFF);

// Passing in an NkColor struct
NkColor red = NkColor.create().r((byte)0xFF).a((byte)0xFF);
context.style().text().color().set(red);

// Reading color values
int r = context.style.text().color().r();

/* Adding padding to the text */
// Make sure that the height of the row can accommodate the padding, otherwise
// the text may be pushed out of view.
context.style().text().padding().set(10, 10);

// Reading padding values
float x = context.style().text().padding().x();

Text Field

A text field allows a user to type in text. To create a text field, call the nk_edit_string() function. You can pass in flags to set some field options, here are the flags you can use:

  • NK_EDIT_FIELD - Make an ordinary text field
  • NK_EDIT_MULTILINE - Displays a text area that allows line breaks instead of a single-line text field
  • NK_EDIT_NO_CURSOR - The user cannot move the text cursor to the middle of the string
  • NK_EDIT_SELECTABLE - The user is able to select a portion of the text
  • NK_EDIT_GOTO_END_ON_ACTIVATE - Starts the cursor at the end of the string when the user focuses the field.

The text field uses a C-style string, where the content of the string and its length are in two different variables. The nk_edit_string() function writes the string data itself into a ByteBuffer and the length of that string in an IntBuffer of size 1. We must give it a ByteBuffer that is large enough to hold all the text that we expect the user to write, because the ByteBuffer cannot change size after it has been created. The function requires us to pass in a maximum string length, we can set the size of the ByteBuffer to this value if we're only using ASCII characters. If you want more characters then I would recommend making the ByteBuffer four times the size of the length, so that the string can have up to four bytes per character. When we want to read the string data we have to use the length provided by the IntBuffer to read a specific number of bytes from the ByteBuffer and then transform those into a string.

The last parameter of the nk_edit_string() function is a filter. The text field uses filters to limit the type of characters that user can write. You can pass in null to the filter, but I encountered an issue when I tried that. During testing, I found that special characters like "é" cause the program to crash so I had to limit my characters to ASCII using a filter. This works for me because I only needed ASCII characters but, unfortunately, I cannot say how to solve this for mullti-byte characters yet. If I find out, I will update this document. In Java, filters seem to be a reference to a function. The following is a list of available pre-built filters:

  • Nuklear::nnk_filter_ascii - Non-ASCII characters are ignored
  • Nuklear::nnk_filter_binary - I'm not sure about this one, sorry
  • Nuklear::nnk_filter_decimal - Restricted to digits 0 to 9
  • Nuklear::nnk_filter_default - I'm not sure about this one either, it doesn't seem to do anything.
  • Nuklear::nnk_filter_float - Restricted to digits 0-9, ., - and +. Possibly "e" and "E" as well.
  • Nuklear::nnk_filter_hex - Restricted to 0-9 and A-F
  • Nuklear::nnk_filter_oct - Restricted to 0-7

Given all of the above information, here is an example of how to make a class that will use Nuklear to add a text field and allow us to read its value.

public class TextField {
    private final int options;
    private final int maxLength;
    private ByteBuffer content; // Nuklear puts the data in here
    private IntBuffer length; // Nuklear writes the length of the string in here
    private NkPluginFilterI filter; // Restrict what the user can type

    public TextField(int maxLength, boolean multiline) {
        this.maxLength = maxLength;
        options = 0;
        if(multiline) {
            options |= NK_EDIT_MULTILINE;
        }
        // Since we're using ASCII, each character just takes one byte.
        // We use maxLength + 1 because Nuklear seems to omit the last character.
        content = BufferUtils.createByteBuffer(maxLength + 1);

        // The IntBuffer is size 1 because we only need one int
        length = BufferUtils.createIntBuffer(1); // BufferUtils from LWJGL

        // Setup a filter to restrict to ASCII
        filter = NkPluginFilter.create(Nuklear::nnk_filter_ascii);
    }

    /**
     * This method uses Nuklear to draw the text field
     */
    public void render() {
        // We use maxLength + 1 because Nuklear seems to omit the last character
        nk_edit_string(context, options, content, length, maxLength+1, filter);
    }

    /**
     * This method returns the text that the user typed in
     */
    public String getValue() {
        // The way to get a string from a ByteBuffer is to pull out an array of
        // bytes and pass it into the String constructor.
        value.mark(); // Mark the buffer so that we can return the pointer here when we're done
        byte[] bytes = new byte[length.get(0)];
        content.get(bytes, 0, length.get(0));
        content.reset(); // Return to the previous marker so that Nuklear can write here again
        String out = new String(bytes, Charset.forName("ASCII"));
        return out;
    }
}

I encountered an issue where, if the user is on a laptop and taps the touchpad quickly on the text field it does not get focused. If you want to manually focus a text field so that the user can begin typing in it, you can call nk_edit_focus(). Here's the solution I implemented:

if(nk_widget_is_mouse_clicked(context, NK_BUTTON_LEFT)) {
    nk_edit_focus(context, NK_EDIT_DEFAULT);
}
nk_edit_string(context, options, content, length, maxLength+1, filter);

The nk_edit_string() function returns an int which is composed of several flags. These are the flags it might contain:

  • NK_EDIT_ACTIVE - The text field is currently focused
  • NK_EDIT_INACTIVE - The text field is not focused
  • NK_EDIT_ACTIVATED - The text field has just received focus
  • NK_EDIT_DEACTIVATED - The text field has just lost focus
  • NK_EDIT_COMMITED - The user pressed Enter to submit the text in the field

Here is a short example describing how to use the returned value:

int action = nk_edit_string(context, NK_EDIT_FIELD | NK_EDIT_GOTO_END_ON_ACTIVATE, content, length, maxLength+1, filter);
boolean hasChanged = (action & (NK_EDIT_COMMITED | NK_EDIT_DEACTIVATED)) > 0;
if(hasChanged) {
  System.out.println("Text field has changed");
}

Styling

See the styling guide before reading this section.

You can style the background color and image, border color and thickness and the color of the text. You can also set the color of the cursor and the color of the text highlighted by the cursor. The cursor's size can also be set, but only shows the selected size when it is after the end of the string not highlighting any text. See the following examples:

/* Create colors and textures for use in styles */
// The variable textureID comes from OpenGL
NkColor white = NkColor.create().set((byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF);
NkImage image = NkImage.create();
image.handle(it -> it.id(textureID)); // See NkImage for details

/* Color and image for unfocused text fields */
context.style().edit().normal().data().color().set((byte) 0xF0, (byte) 0xF0, (byte) 0xF0, (byte) 0xFF); // Passing in color values (Red, Green, Blue, Alpha)
context.style().edit().normal().data().image().set(image);

/* Color and image for hovering over unfocused text fields */
context.style().edit().hover().data().color().set((byte) 0, (byte) 0, (byte) 0xFF, (byte) 0xFF); // Passing in color values (Red, Green, Blue, Alpha)
context.style().edit().hover().data().image().set(image);

/* Color and image for focused text fields */
context.style().edit().active().data().color().set(white); // Passing in a color struct
context.style().edit().active().data().image().set(image);

/* Border color and thickness */
context.style().edit().border(5); // Border thickness as a floating point number
context.style().edit().border_color().set((byte) 0, (byte) 0, (byte) 0, (byte) 0xFF); // Passing in color values (Red, Green, Blue, Alpha)

/* Cursor styles */
// The width of the cursor in pixels (when it is not highlighting any text)
context.style().edit().cursor_size(20);

// The cursor_normal() property does not seem to do anything, instead cursor_hover() does that job.
context.style().edit().cursor_normal().set((byte) 40, (byte) 40, (byte) 40, (byte) 0xFF);
context.style().edit().cursor_hover().set((byte) 0, (byte) 0, (byte) 255, (byte) 0xFF);

// The cursor_text_normal() property does not seem to do anything, instead cursor_text_hover() does that job.
context.style().edit().cursor_text_normal().set((byte) 0xFF, (byte) 0, (byte) 0, (byte) 0xFF);
context.style().edit().cursor_text_hover().set((byte) 0xFF, (byte) 0, (byte) 0, (byte) 0xFF);

Button

A button is an element that the user can click on and it makes that click information available to your program. You can write code to perform an action when the button is clicked. When the button is clicked the function that rendered the button returns true, otherwise it returns false.

A button is only considered clicked when the left mouse button is released. If you want to listen for press, down, and release actions then you will need to use the nk_widget_has_mouse_click_down() function, which I'll explain in the button examples.

A button has either text or an image, each of these types of button has its own function.

Text buttons

Nuklear has two different functions for buttons with text and I could not find any difference between the two. The first function is nk_button_label() and the second is nk_button_text(). Since I cannot find any difference between them, I am documenting just the first one and sticking with it for consistency.

if(nk_button_label(context, "Click Here!")) {
    //
    // Action to be performed when the button is clicked
    //
}

Image buttons

A button may contain an image instead of text. The image passed in is a struct of type NkImage, see the documentation for NkImage to learn how to create one.

The image button has three different functions. The first one, nk_button_image(), creates a button with an images in it while the second two, nk_button_image_label() and nk_button_image_text(), both create a button with an image and text. Just like with the text button, these last two functions are both identical, so I will only be documenting one of them. The nk_button_image_label() function takes as arguments the context, the NkImage struct, the button text and some flags for button text alignment. The following flags can be used:

  • NK_TEXT_ALIGN_LEFT - Left aligned
  • NK_TEXT_ALIGN_RIGHT - Right aligned
  • NK_TEXT_ALIGN_CENTERED - Horizontally centered
  • NK_TEXT_ALIGN_TOP - Aligned to the top of the row
  • NK_TEXT_ALIGN_BOTTOM - Aligned to the bottom of the row
  • NK_TEXT_ALIGN_MIDDLE - Vertically centered

The following example shows how to create an image button. The code to generate the NkImage struct should be outside of the main program loop, before the loop starts.

// OpenGL texture code
int textureID = glGenTextures();
//
// Load the texture here
//

NkImage image = NkImage.create();
image.handle(it -> it.id(textureID)); // See NkImage for details

// This is the program loop, see it in the Setup section of this document
while (!glfwWindowShouldClose(win)) {

    // ...

    if(nk_button_image(context, buttonImage)) {
        // Action to be performed when the button is clicked
    }

    if(nk_button_image_label(context, buttonImage, "Click Here!", NK_TEXT_ALIGN_LEFT | NK_TEXT_ALIGN_MIDDLE)) {
        // Action to be performed when the button is clicked
    }

    // ...

}

How to capture press, down and release actions on the button

By default the button only ever returns true when the left mouse button is released. If we want to check for a press or down event we will have to make use of the nk_widget_has_mouse_click_down() function. This function returns a boolean based on the mouse state of the next widget in the code. It must be magic that it can read the future, perhaps it keeps the state from the previous loop iteration. You have to tell it which boolean to return for the down state, it actually lets you choose that by passing the desired boolean as one of its arguments, I'm not sure why.

In order to check whether the button has been pressed or released, we need to keep a record of which value it had in the previous loop iteration. If the previous value was false and the current value is true then the button was just pressed. If the previous value was true and the current value is false then the button was just released. If both of the values are true then the button is currently down. I've abstracted this into its own class for convenience, instances of this class should be created outside of the loop and the render method should be called on each loop iteration, with the isPressed(), isReleased() and isDown() being tested after render() has been called.

public class TextButton {
    private NkContext context;
    private boolean wasPressed = false;
    private boolean isPressed = false;
    private final String text;
    
    public TextButton(NkContext context, String text) {
        this.context = context;
        this.text = text;
    }

    public boolean isPressed() { return isPressed && !wasPressed; }
    public boolean isReleased() { return !isPressed && wasPressed; }
    public boolean isDown() { return isPressed; }

    public void render() {
        // Store the previous button state
        wasPressed = isPressed;

        // Get the current button state
        isPressed = nk_widget_has_mouse_click_down(context, NK_BUTTON_LEFT, true);

        if(nk_button_label(context, text) && !isPressed && !wasPressed) {
            // This code runs when the button is tapped in just one frame
            wasPressed = true;
            isPressed = false;
        }
    }
    
}

Styling buttons

See the styling guide before reading this section.

Buttons have a background color, a background image, a border color and thickness, padding and a rounding radius for corners.

Number Fields

Number fields provide a convenient way for the user to input numbers. The number can be changed either by typing, clicking on arrows, or by pressing the mouse button down on a part of the the field's surface which does not have numbers printed on it and dragging the mouse.

There are three types of number fields: Integer, Float and Double. They all work in about the same way, the only difference is the precision of the number being displayed.

All of the fields use the nk_property_*() functions where * indicates the data type:
For int, the function is nk_property_int().
For float, the function is nk_property_float().
For double, the function is nk_property_double().

The nk_property_* functions takes six arguments in addition to the context:

  • String label - The name of the field. This is shown next to the number.
  • (int, float, double) min - The minimum permitted value for the field.
  • (int[], float[], double[]) value - An array of size 1 in which the value is read and written.
  • (int, float, double) max - The maximum permitted value for the field.
  • (int, float, double) step - The amount that the value is changed when an arrow is clicked.
  • float incPerPixel - The amount that the value is changed per pixel of movement when the mouse is being dragged along the widget.

The label has a peculiar problem: It is used to identify the widget as well as display the name. This means that by default only one element can have the same label. Nuklear has a fix for this, by prefixing the label with "#" each label is managed in a way that Nuklear handles the widget's unique identifier internally. The "#" character will not be displayed in the widget shown on the screen.

The following is an example of a simple abstraction layer you can use for a float field:

public class FloatField {
    private String label;
    private NkContext context;
    private final float[] value = new float[1];
    private float min = 0;
    private float max = 0;
    private float step = 0.01f;
    private float incPerPixel = 0.001f;

    public FloatField(NkContext context, String label, float min, float value, float max, float step, float incPerPixel) {
        this.context = context;
        this.label = label;
        this.min = min;
        this.value[0] = value;
        this.max = max;
        this.step = step;
        this.incPerPixel = incPerPixel;
    }

    public void setValue(float value) {
        this.value[0] = value;
    }

    public float getValue() {
        return value[0];
    }
        
    /**
     * 
     * @return True if the field has changed, false otherwise.
     */
    public boolean render() {
        float before = value[0];
        nk_property_float(context, label, min, value, max, step, incPerPixel);
        return before != value[0];
    }
}

The following is an example of how the above class could be used in the main program loop:

// Create a float field outside of the main loop
FloatField height = new FloatField(context, "Height", 0, 1, 10, 0.1, 0.1);

// This is the program loop, see it in the Setup section of this document
while (!glfwWindowShouldClose(win)) {

    // ...

    boolean changed = height.render();
    if(changed) {
        // Print the value that the user entered into the field
        float h = height.getValue();
        System.out.println(h);
    }

    // ...

}