Implementing GUIs using Clojure and LWJGL Nuklear bindings

Nuklear GUI

For game development I have been using LWJGL3 which is a great Java library for cross-platform development. Among other things it has bindings for OpenGL, GLFW, and STB. Recently I discovered that it also has Nuklear bindings. Nuklear is a small C library useful for developing GUIs for games. It receives control input and commands to populate a GUI and converts those into render instructions. Nuklear focuses solely on the user interface, while input and graphics backend are handled by the application. It is therefore very flexible and can be integrated into a 3D game implemented using OpenGL, DirectX, Vulkan, or other.

LWJGL Nuklear bindings come with the GLFWDemo.java example. In this article I have basically translated the input and graphics backend to Clojure. I also added examples for several different controls. I have pushed the source code to Github if you want to look at it straight away. A big thank you to Ioannis Tsakpinis who developed LWJGL and GLFWDemo.java in particular. And a big thank you to Micha Mettke who developed the Nuklear library.

The demo is more than 400 lines of code. This is because it has to implement the graphics backend, input conversion, and truetype font conversion to bitmap font. If you are rather looking for a Clojure GUI library which does not require you to do this, you might want to look at HumbleUI. There also is Quil which seems to be more about graphics and animations.

Dependencies

Here is the deps.edn file:

{:deps {org.clojure/clojure {:mvn/version "1.11.3"}
        org.lwjgl/lwjgl {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl$natives-linux {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-opengl {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-opengl$natives-linux {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-nuklear {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-nuklear$natives-linux {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-glfw {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-glfw$natives-linux {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-stb {:mvn/version "3.3.3"}
        org.lwjgl/lwjgl-stb$natives-linux {:mvn/version "3.3.3"}}
 :paths ["."]}

The file contains dependencies to:

  • LWJGL core library
  • OpenGL for rendering
  • Nuklear for the GUI
  • GLFW for creating a window and handling input
  • STB for loading images and for converting a truetype font to a texture

If you are not using the natives-linux bindings, there are more native packages for lwjgl-opengl such as natives-windows and natives-macos.

Graphics Setup

GLFW Window

We start with a simple program showing a window which can be closed by the user. Here is the initial nukleartest.clj file:

(ns nukleartest
    (:import [org.lwjgl.glfw GLFW]
             [org.lwjgl.opengl GL]))

(def width 640)
(def height 640)

(GLFW/glfwInit)

(GLFW/glfwDefaultWindowHints)
(GLFW/glfwWindowHint GLFW/GLFW_RESIZABLE GLFW/GLFW_FALSE)
(def window (GLFW/glfwCreateWindow width height "Nuklear Example" 0 0))

(GLFW/glfwMakeContextCurrent window)
(GLFW/glfwShowWindow window)
(GL/createCapabilities)
(GLFW/glfwSwapInterval 1)

(while (not (GLFW/glfwWindowShouldClose window))
       (GLFW/glfwPollEvents)
       (GLFW/glfwSwapBuffers window))

(GLFW/glfwTerminate)

(System/exit 0)

You can run the program using clj -M -m nukleartest and it should show a blank window.

GLFW Window

OpenGL Shader Program

Next thing is to add initialisation of an OpenGL shader program to be used in the rendering backend. The code is similar to my earlier post showing an OpenGL Clojure example.

(ns nukleartest
    (:import [org.lwjgl.glfw GLFW]
             [org.lwjgl.opengl GL GL20]))

; ...

(def vertex-source
"#version 410 core
uniform mat4 projection;
in vec2 position;
in vec2 texcoord;
in vec4 color;
out vec2 frag_uv;
out vec4 frag_color;
void main()
{
  frag_uv = texcoord;
  frag_color = color;
  gl_Position = projection * vec4(position, 0, 1);
}")

(def fragment-source
"#version 410 core
uniform sampler2D tex;
in vec2 frag_uv;
in vec4 frag_color;
out vec4 out_color;
void main()
{
  out_color = frag_color * texture(tex, frag_uv);
}")

(def program (GL20/glCreateProgram))

(def vertex-shader (GL20/glCreateShader GL20/GL_VERTEX_SHADER))
(GL20/glShaderSource vertex-shader vertex-source)
(GL20/glCompileShader vertex-shader)
(when (zero? (GL20/glGetShaderi vertex-shader GL20/GL_COMPILE_STATUS))
  (println (GL20/glGetShaderInfoLog vertex-shader 1024))
  (System/exit 1))

(def fragment-shader (GL20/glCreateShader GL20/GL_FRAGMENT_SHADER))
(GL20/glShaderSource fragment-shader fragment-source)
(GL20/glCompileShader fragment-shader)
(when (zero? (GL20/glGetShaderi fragment-shader GL20/GL_COMPILE_STATUS))
  (println (GL20/glGetShaderInfoLog fragment-shader 1024))
  (System/exit 1))

(GL20/glAttachShader program vertex-shader)
(GL20/glAttachShader program fragment-shader)
(GL20/glLinkProgram program)
(when (zero? (GL20/glGetProgrami program GL20/GL_LINK_STATUS))
  (println (GL20/glGetProgramInfoLog program 1024))
  (System/exit 1))
(GL20/glDeleteShader vertex-shader)
(GL20/glDeleteShader fragment-shader)

; ...

(GL20/glDeleteProgram program)

; ...

vertex and fragment shader

The vertex shader passes through texture coordinates and fragment colors. Furthermore it scales the input position to OpenGL normalized device coordinates (we will set the projection matrix later). The fragment shader performs a texture lookup and multiplies the result with the fragment color value. The Clojure code compiles and links the shaders and checks for possible errors.

Vertex Array Object

Next an OpenGL vertex array object is defined.

(ns nukleartest
    (:import [org.lwjgl.glfw GLFW]
             [org.lwjgl.opengl GL GL11 GL15 GL20 GL30]))

; ...

(GL20/glUseProgram program)

(def position (GL20/glGetAttribLocation program "position"))
(def texcoord (GL20/glGetAttribLocation program "texcoord"))
(def color (GL20/glGetAttribLocation program "color"))

(def vbo (GL15/glGenBuffers))
(def ebo (GL15/glGenBuffers))
(def vao (GL30/glGenVertexArrays))

(GL30/glBindVertexArray vao)
(GL15/glBindBuffer GL15/GL_ARRAY_BUFFER vbo)
(GL15/glBindBuffer GL15/GL_ELEMENT_ARRAY_BUFFER ebo)

(GL20/glEnableVertexAttribArray position)
(GL20/glEnableVertexAttribArray texcoord)
(GL20/glEnableVertexAttribArray color)

(GL20/glVertexAttribPointer position 2 GL11/GL_FLOAT false 20 0)
(GL20/glVertexAttribPointer texcoord 2 GL11/GL_FLOAT false 20 8)
(GL20/glVertexAttribPointer color 4 GL11/GL_UNSIGNED_BYTE true 20 16)

; ...

(GL30/glBindVertexArray 0)
(GL15/glBindBuffer GL15/GL_ARRAY_BUFFER 0)
(GL15/glBindBuffer GL15/GL_ELEMENT_ARRAY_BUFFER 0)

(GL30/glDeleteVertexArrays vao)
(GL15/glDeleteBuffers ebo)
(GL15/glDeleteBuffers vbo)

; ...

An array buffer containing the position, texture coordinates, and colors are allocated. Furthermore an element array buffer is allocated which contains element indices. A row in the array buffer contains 20 bytes:

  • 2 times 4 bytes for floating point “position”
  • 2 times 4 bytes for floating point texture coordinate “texcoord”
  • 4 bytes for RGBA color value “color”

vertex buffer object

The Nuklear library needs to be configured with the same layout of the vertex array buffer. For this purpose a Nuklear vertex layout object is initialised using the NK_VERTEX_ATTRIBUTE_COUNT attribute as a terminator:

(ns nukleartest
    (:import ; ...
             [org.lwjgl.nuklear Nuklear NkDrawVertexLayoutElement]))

; ...

(def vertex-layout (NkDrawVertexLayoutElement/malloc 4))
(-> vertex-layout (.position 0) (.attribute Nuklear/NK_VERTEX_POSITION)
    (.format Nuklear/NK_FORMAT_FLOAT) (.offset 0))
(-> vertex-layout (.position 1) (.attribute Nuklear/NK_VERTEX_TEXCOORD)
    (.format Nuklear/NK_FORMAT_FLOAT) (.offset 8))
(-> vertex-layout (.position 2) (.attribute Nuklear/NK_VERTEX_COLOR)
    (.format Nuklear/NK_FORMAT_R8G8B8A8) (.offset 16))
(-> vertex-layout (.position 3)
    (.attribute Nuklear/NK_VERTEX_ATTRIBUTE_COUNT)
    (.format Nuklear/NK_FORMAT_COUNT) (.offset 0))
(.flip vertex-layout)

; ...

Null Texture

For drawing flat colors using the shader program above, Nuklear needs to specify a null texture.

(ns nukleartest
    (:import [org.lwjgl BufferUtils]
             ; ...
             [org.lwjgl.nuklear Nuklear NkDrawVertexLayoutElement
              NkDrawNullTexture]))

; ...

(def null-tex (GL11/glGenTextures))
(GL11/glBindTexture GL11/GL_TEXTURE_2D null-tex)
(def buffer (BufferUtils/createIntBuffer 1))
(.put buffer (int-array [0xFFFFFFFF]))
(.flip buffer)
(GL11/glTexImage2D GL11/GL_TEXTURE_2D 0 GL11/GL_RGBA8 1 1 0 GL11/GL_RGBA
                   GL11/GL_UNSIGNED_BYTE buffer)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MIN_FILTER
                      GL11/GL_NEAREST)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MAG_FILTER
                      GL11/GL_NEAREST)

(def null-texture (NkDrawNullTexture/create))
(.id (.texture null-texture) null-tex)
(.set (.uv null-texture) 0.5 0.5)

; ...

(GL11/glDeleteTextures null-tex)

; ...

The null texture basically just consists of a single white pixel so that the shader term texture(tex, frag_uv) evaluates to vec4(1, 1, 1, 1). The Nuklear null texture uses fixed texture coordinates for lookup (here: 0.5, 0.5). I.e. it is possible to embed the null texture in a bigger multi-purpose texture to save a texture slot.

Nuklear GUI

Nuklear Context, Command Buffer, and Configuration

Finally we can set up a Nuklear context object “context”, a render command buffer “cmds”, and a rendering configuration “config”. Nuklear even delegates allocating and freeing up memory, so we need to register callbacks for that as well.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.nuklear ; ...
              NkAllocator NkContext NkPluginAllocI NkPluginFreeI NkUserFont
              NkBuffer NkConvertConfig]
             [org.lwjgl.system MemoryUtil MemoryStack]))

; ...

(def buffer-initial-size (* 4 1024))

(def stack (MemoryStack/stackPush))

; ...

(def allocator (NkAllocator/create))
(.alloc allocator
  (reify NkPluginAllocI
         (invoke [this handle old size] (MemoryUtil/nmemAllocChecked size))))
(.mfree allocator
  (reify NkPluginFreeI
         (invoke [this handle ptr] (MemoryUtil/nmemFree ptr))))

(def context (NkContext/create))

(def font (NkUserFont/create))
(Nuklear/nk_init context allocator font)

(def cmds (NkBuffer/create))
(Nuklear/nk_buffer_init cmds allocator buffer-initial-size)

(def config (NkConvertConfig/calloc stack))

(doto config
      (.vertex_layout vertex-layout)
      (.vertex_size 20)
      (.vertex_alignment 4)
      (.tex_null null-texture)
      (.circle_segment_count 22)
      (.curve_segment_count 22)
      (.arc_segment_count 22)
      (.global_alpha 1.0)
      (.shape_AA Nuklear/NK_ANTI_ALIASING_ON)
      (.line_AA Nuklear/NK_ANTI_ALIASING_ON))

; ...

(Nuklear/nk_free context)

(.free (.alloc allocator))
(.free (.mfree allocator))

; ...

We also created an empty font object which we will initialise properly later.

Setup Rendering

OpenGL needs to be configured for rendering the Nuklear GUI.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.opengl GL GL11 GL13 GL14 GL15 GL20 GL30]
             ; ...
             ))

; ...

(GL11/glEnable GL11/GL_BLEND)
(GL14/glBlendEquation GL14/GL_FUNC_ADD)
(GL14/glBlendFunc GL14/GL_SRC_ALPHA GL14/GL_ONE_MINUS_SRC_ALPHA)
(GL11/glDisable GL11/GL_CULL_FACE)
(GL11/glDisable GL11/GL_DEPTH_TEST)
(GL11/glEnable GL11/GL_SCISSOR_TEST)
(GL13/glActiveTexture GL13/GL_TEXTURE0)

(def projection (GL20/glGetUniformLocation program "projection"))
(def buffer (BufferUtils/createFloatBuffer 16))
(.put buffer (float-array [(/ 2.0 width) 0.0 0.0 0.0,
                           0.0 (/ -2.0 height) 0.0 0.0,
                           0.0 0.0 -1.0 0.0,
                           -1.0 1.0 0.0 1.0]))
(.flip buffer)
(GL20/glUniformMatrix4fv projection false buffer)

; ...
  • Blending with existing pixel data is enabled and the blending equation and function are set
  • Culling of back or front faces is disabled
  • Depth testing is disabled
  • Scissor testing is enabled
  • The first texture slot is enabled

Also the uniform projection matrix for mapping pixel coordinates [0, width] x [0, height] to [-1, 1] x [-1, 1] is defined. The projection matrix also flips the y-coordinates since the direction of the OpenGL y-axis is reversed in relation to the pixel y-coordinates.

Converting window coordinates to normalized device coordinates

Minimal Test GUI

Now we will add a minimal GUI just using a progress bar for testing rendering without fonts.

(ns nukleartest
    (:import [org.lwjgl BufferUtils PointerBuffer]
             ; ...
             [org.lwjgl.nuklear ; ...
              NkRect]
             ; ...
             ))

; ...

(def rect (NkRect/malloc stack))

(def progress (PointerBuffer/allocateDirect 1))

(while (not (GLFW/glfwWindowShouldClose window))
       (Nuklear/nk_input_begin context)
       (GLFW/glfwPollEvents)
       (Nuklear/nk_input_end context)
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         (.put progress 0 (mod (inc (.get progress 0)) 100))
         (Nuklear/nk_layout_row_dynamic context 32 1)
         (Nuklear/nk_progress context progress 100 true)
         (Nuklear/nk_end context))
       ; ...
       )

; ...

First we set up a few values and then in the main loop we start Nuklear input using Nuklear/nk_input_begin, call GLFW to process events, and then end Nuklear input. We will implement the GLFW callbacks to convert events to Nuklear calls later.

We start populating the GUI by calling Nuklear/nk_begin thereby specifying the window size. We increase the progress value and store it in a PointerBuffer object. The call (Nuklear/nk_layout_row_dynamic context 32 1) sets the GUI layout to 32 pixels height and one widget per row. Then a progress bar is created and the GUI is finalised using Nuklear/nk_end.

Rendering Backend

Now we are ready to add the rendering backend.

; ...

(def max-vertex-buffer (* 512 1024))
(def max-element-buffer (* 128 1024))

; ...

(GL15/glBufferData GL15/GL_ARRAY_BUFFER max-vertex-buffer
 GL15/GL_STREAM_DRAW)
(GL15/glBufferData GL15/GL_ELEMENT_ARRAY_BUFFER max-element-buffer
 GL15/GL_STREAM_DRAW)

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (GL11/glViewport 0 0 width height)
       (let [vertices (GL15/glMapBuffer GL15/GL_ARRAY_BUFFER
                                        GL15/GL_WRITE_ONLY max-vertex-buffer
                                        nil)
             elements (GL15/glMapBuffer GL15/GL_ELEMENT_ARRAY_BUFFER
                                        GL15/GL_WRITE_ONLY max-element-buffer
                                        nil)
             stack    (MemoryStack/stackPush)
             vbuf     (NkBuffer/malloc stack)
             ebuf     (NkBuffer/malloc stack)]
         (Nuklear/nk_buffer_init_fixed vbuf vertices)
         (Nuklear/nk_buffer_init_fixed ebuf elements)
         (Nuklear/nk_convert context cmds vbuf ebuf config)
         (GL15/glUnmapBuffer GL15/GL_ELEMENT_ARRAY_BUFFER)
         (GL15/glUnmapBuffer GL15/GL_ARRAY_BUFFER)
         (loop [cmd (Nuklear/nk__draw_begin context cmds) offset 0]
               (when cmd
                 (when (not (zero? (.elem_count cmd)))
                   (GL11/glBindTexture GL11/GL_TEXTURE_2D
                                       (.id (.texture cmd)))
                   (let [clip-rect (.clip_rect cmd)]
                     (GL11/glScissor (int (.x clip-rect))
                                     (int (- height
                                             (int (+ (.y clip-rect)
                                                     (.h clip-rect)))))
                                     (int (.w clip-rect))
                                     (int (.h clip-rect))))
                   (GL11/glDrawElements GL11/GL_TRIANGLES (.elem_count cmd)
                                        GL11/GL_UNSIGNED_SHORT offset))
                 (recur (Nuklear/nk__draw_next cmd cmds context)
                        (+ offset (* 2 (.elem_count cmd))))))
         (Nuklear/nk_clear context)
         (Nuklear/nk_buffer_clear cmds)
         (GLFW/glfwSwapBuffers window)
         (MemoryStack/stackPop)))

; ...

The rendering backend sets the viewport and then array buffers for the vertex data and the indices are allocated. Then the buffers are mapped to memory resulting in the two java.nio.DirectByteBuffer objects “vertices” and “elements”. The two static buffers are then converted to Nuklear buffer objects using Nuklear/nk_buffer_init_fixed.

Then the core method of the Nuklear library Nuklear/nk_convert is called. It populates the (dynamic) command buffer “cmds” which we initialised earlier as well as the mapped vertex buffer and index buffer. After the conversion, the two OpenGL memory mappings are undone.

A Clojure loop then is used to get chunks of type NkDrawCommand from the render buffer. Each draw command requires setting the texture id and the clipping region. Then a part of the index and vertex buffer is rendered using GL11/glDrawElements.

Finally Nuklear/nk_clear is used to reset the GUI specification for the next frame and Nuklear/nk_buffer_clear is used to empty the command buffer. GLFW/glfwSwapBuffers is used to publish the new rendered frame.

Now we finally have a widget working!

GUI showing a progress bar

Mouse Events

Cursor Position and Buttons

The next step one can do is converting GLFW mouse events to Nuklear input.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.glfw GLFW GLFWCursorPosCallbackI
              GLFWMouseButtonCallbackI]
             ; ...
             ))

; ...

(GLFW/glfwSetCursorPosCallback
  window
  (reify GLFWCursorPosCallbackI
         (invoke [this window xpos ypos]
           (Nuklear/nk_input_motion context (int xpos) (int ypos)))))

(GLFW/glfwSetMouseButtonCallback
  window
  (reify GLFWMouseButtonCallbackI
         (invoke [this window button action mods]
           (let [stack (MemoryStack/stackPush)
                 cx    (.mallocDouble stack 1)
                 cy    (.mallocDouble stack 1)]
             (GLFW/glfwGetCursorPos window cx cy)
             (let [x        (int (.get cx 0))
                   y        (int (.get cy 0))
                   nkbutton (cond
                              (= button GLFW/GLFW_MOUSE_BUTTON_RIGHT)
                                Nuklear/NK_BUTTON_RIGHT
                              (= button GLFW/GLFW_MOUSE_BUTTON_MIDDLE)
                                Nuklear/NK_BUTTON_MIDDLE
                              :else
                                Nuklear/NK_BUTTON_LEFT)]
               (Nuklear/nk_input_button context nkbutton x y
                                        (= action GLFW/GLFW_PRESS))
               (MemoryStack/stackPop))))))

; ...

The first callback is to process mouse cursor movement events. The second callback converts mouse button press and release events to Nuklear input. The progress bar is modifyable and you should now be able to change it by clicking on it. Note that using a case statement instead of cond did not work for some reason.

Scroll Events

The Nuklear library can also be informed about scroll events. Here is the corresponding GLFW callback to pass scroll events on to the Nuklear library.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.glfw ; ...
              GLFWScrollCallbackI]
             ; ...
             ))

; ...

(GLFW/glfwSetScrollCallback
  window
  (reify GLFWScrollCallbackI
         (invoke [this window xoffset yoffset]
           (let [stack (MemoryStack/stackPush)
                 scroll (NkVec2/malloc stack)]
             (.x scroll xoffset)
             (.y scroll yoffset)
             (Nuklear/nk_input_scroll context scroll)
             (MemoryStack/stackPop)))))

; ...

The scroll events can later be tested when we implement a combo box.

Fonts

Converting Truetype Font to Bitmap Font

To display other GUI controls, text output is required. Using the STB library a Truetype font is converted to a bitmap font of the desired size.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.stb STBTruetype STBTTFontinfo STBTTPackedchar
              STBTTPackContext STBImageWrite]))

; ...

(def font-height 18)
(def bitmap-w 512)
(def bitmap-h 512)

; ...

(def font (NkUserFont/create))

(def ttf-in (clojure.java.io/input-stream "FiraSans.ttf"))
(def ttf-out (java.io.ByteArrayOutputStream.))
(clojure.java.io/copy ttf-in ttf-out)
(def ttf-bytes (.toByteArray ttf-out))
(def ttf (BufferUtils/createByteBuffer (count ttf-bytes)))
(.put ttf ttf-bytes)
(.flip ttf)

(def fontinfo (STBTTFontinfo/create))
(def cdata (STBTTPackedchar/calloc 95))

(STBTruetype/stbtt_InitFont fontinfo ttf)
(def scale (STBTruetype/stbtt_ScaleForPixelHeight fontinfo font-height))

(def d (.mallocInt stack 1))
(STBTruetype/stbtt_GetFontVMetrics fontinfo nil d nil)
(def descent (* (.get d 0) scale))
(def bitmap (MemoryUtil/memAlloc (* bitmap-w bitmap-h)))
(def pc (STBTTPackContext/malloc stack))
(STBTruetype/stbtt_PackBegin pc bitmap bitmap-w bitmap-h 0 1 0)
(STBTruetype/stbtt_PackSetOversampling pc 4 4)
(STBTruetype/stbtt_PackFontRange pc ttf 0 font-height 32 cdata)
(STBTruetype/stbtt_PackEnd pc)

(def texture (MemoryUtil/memAlloc (* bitmap-w bitmap-h 4)))
(def data (byte-array (* bitmap-w bitmap-h)))
(.get bitmap data)
(def data (int-array (mapv #(bit-or (bit-shift-left % 24) 0x00FFFFFF) data)))
(def texture-int (.asIntBuffer texture))
(.put texture-int data)

; (STBImageWrite/stbi_write_png "font.png" bitmap-w bitmap-h 4 texture
;                               (* 4 bitmap-w))

; ...

Basically the font file is read and converted to a java.nio.DirectByteBuffer (let me know if you find a more straightforward way to do this). The data is used to initialise an STB font info object. The next steps I can’t explain in detail but they basically pack the glyphs into a greyscale bitmap.

Finally a white RGBA texture data is created with the greyscale bitmap as the alpha channel. You can write out the RGBA data to a PNG file and inspect it using GIMP or your favourite image editor.

Bitmap font created with STB library

Font Texture and Nuklear Callbacks

The RGBA bitmap font can now be converted to an OpenGL texture with linear interpolation and the texture id of the NkUserFont object is set.

(ns nukleartest
    (:import [org.lwjgl.nuklear ; ...
              NkHandle]
             ; ...
             ))

; ...

(def font-tex (GL11/glGenTextures))
(GL11/glBindTexture GL11/GL_TEXTURE_2D font-tex)
(GL11/glTexImage2D GL11/GL_TEXTURE_2D 0 GL11/GL_RGBA8 bitmap-w bitmap-h 0
                   GL11/GL_RGBA GL11/GL_UNSIGNED_BYTE texture)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MIN_FILTER
                      GL11/GL_LINEAR)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MAG_FILTER
                      GL11/GL_LINEAR)
(MemoryUtil/memFree texture)
(MemoryUtil/memFree bitmap)

(def handle (NkHandle/create))
(.id handle font-tex)
(.texture font handle)

; ...

(GL11/glDeleteTextures font-tex)

; ...

To get the font working with Nuklear, callbacks for text width, text height, and glyph region in the texture need to be added.

(ns nukleartest
    (:import [org.lwjgl.nuklear ; ...
              NkTextWidthCallbackI NkQueryFontGlyphCallbackI NkUserFontGlyph]
             ; ...
             [org.lwjgl.stb ; ...
              STBTTAlignedQuad]))

; ...

(.width font
  (reify NkTextWidthCallbackI
    (invoke [this handle h text len]
      (let [stack     (MemoryStack/stackPush)
            unicode   (.mallocInt stack 1)
            advance   (.mallocInt stack 1)
            glyph-len (Nuklear/nnk_utf_decode text
                        (MemoryUtil/memAddress unicode) len)
            result
            (loop [text-len glyph-len glyph-len glyph-len text-width 0.0]
                  (if (or (> text-len len)
                          (zero? glyph-len)
                          (= (.get unicode 0) Nuklear/NK_UTF_INVALID))
                    text-width
                    (do
                      (STBTruetype/stbtt_GetCodepointHMetrics fontinfo
                        (.get unicode 0) advance nil)
                      (let [text-width (+ text-width
                                          (* (.get advance 0) scale))
                            glyph-len  (Nuklear/nnk_utf_decode
                                         (+ text text-len)
                                         (MemoryUtil/memAddress unicode)
                                         (- len text-len))]
                        (recur (+ text-len glyph-len)
                               glyph-len
                               text-width)))))]
        (MemoryStack/stackPop)
        result))))

(.height font font-height)

(.query font
        (reify NkQueryFontGlyphCallbackI
               (invoke [this handle font-height glyph codepoint
                        next-codepoint]
                 (let [stack   (MemoryStack/stackPush)
                       x       (.floats stack 0.0)
                       y       (.floats stack 0.0)
                       q       (STBTTAlignedQuad/malloc stack)
                       advance (.mallocInt stack 1)]
                   (STBTruetype/stbtt_GetPackedQuad cdata bitmap-w bitmap-h
                                                    (- codepoint 32) x y q
                                                    false)
                   (STBTruetype/stbtt_GetCodepointHMetrics fontinfo
                     codepoint advance nil)
                   (let [ufg (NkUserFontGlyph/create glyph)]
                     (.width ufg (- (.x1 q) (.x0 q)))
                     (.height ufg (- (.y1 q) (.y0 q)))
                     (.set (.offset ufg)
                           (.x0 q)
                           (+ (.y0 q) font-height descent))
                     (.xadvance ufg (* (.get advance 0) scale))
                     (.set (.uv ufg 0) (.s0 q) (.t0 q))
                     (.set (.uv ufg 1) (.s1 q) (.t1 q)))
                   (MemoryStack/stackPop)))))

; ...

I don’t fully understand yet how the “width” and “query” implementations work. Hopefully I find a way to do a unit-tested reimplementation to get a better understanding later.

On a positive note though, at this point it is possible to render text. In the following code we add a button for stopping and a button for starting the progress bar.

; ...

(def progress (PointerBuffer/allocateDirect 1))
(def increment (atom 1))

(while (not (GLFW/glfwWindowShouldClose window))
       (Nuklear/nk_input_begin context)
       (GLFW/glfwPollEvents)
       (Nuklear/nk_input_end context)
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         (.put progress 0 (mod (+ (.get progress 0) @increment) 100))
         (Nuklear/nk_layout_row_dynamic context 32 1)
         (Nuklear/nk_progress context progress 100 true)
         (Nuklear/nk_layout_row_dynamic context 32 2)
         (if (Nuklear/nk_button_label context "Start")
           (reset! increment 1))
         (if (Nuklear/nk_button_label context "Stop")
           (reset! increment 0))
         (Nuklear/nk_end context))
       ; ...
       )

; ...

The GUI now looks like this:

Progress bar with start and stop button

Trying out Widgets

The following code adds a main menu with an exit item to the window.

(ns nukleartest
    (:import [org.lwjgl.nuklear ; ...
              NkVec2]
             ; ...
             ))

; ...

(def menu-size (NkVec2/create))
(.x menu-size 80)
(.y menu-size 40)

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         (Nuklear/nk_menubar_begin context)
         (Nuklear/nk_layout_row_static context 40 40 1)
         (when (Nuklear/nk_menu_begin_label context "Main"
                                      Nuklear/NK_TEXT_LEFT menu-size)
           (Nuklear/nk_layout_row_dynamic context 32 1)
           (if (Nuklear/nk_menu_item_label context "Exit"
                                           Nuklear/NK_TEXT_LEFT)
             (GLFW/glfwSetWindowShouldClose window true))
           (Nuklear/nk_menu_end context))
         (Nuklear/nk_menubar_end context)
         ; ...
         ))

; ...

Here is a screenshot of the result: Menubar

Option Labels

One can add option labels to implement a choice between different options.

; ...

(def option (atom :easy))

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 32 3)
         (if (Nuklear/nk_option_label context "easy"
                                      (= @option :easy))
           (reset! option :easy))
         (if (Nuklear/nk_option_label context "intermediate"
                                      (= @option :intermediate))
           (reset! option :intermediate))
         (if (Nuklear/nk_option_label context "hard"
                                      (= @option :hard))
           (reset! option :hard))
         ; ...
         ))

; ...

Here is a screenshot with option labels showing the three options easy, intermediate, and hard. Option Labels

Check Labels

Check labels are easy to add. They look similar to the option labels, but use squares instead of circles.

; ...

(def flip (atom false))
(def crop (atom false))

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 32 2)
         (reset! flip (Nuklear/nk_check_label context "Flip" @flip))
         (reset! crop (Nuklear/nk_check_label context "Crop" @crop))
         ; ...
         ))

; ...

Here is a screenshot with two check labels. Option Labels

Property Widgets

Property widgets let you change a value by either clicking on the arrows or by clicking and dragging across the widget.

; ...
(def compression (.put (BufferUtils/createIntBuffer 1) 0 20))
(def quality (.put (BufferUtils/createFloatBuffer 1) 0 (float 5.0)))
; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_property_int context "Compression:" 0 compression
                                  100 10 (float 1))
         (Nuklear/nk_property_float context "Quality:" (float 0.0) quality
                                    (float 10.0) (float 1.0) (float 0.01))
         ; ...
         ))

; ...

Here is a screenshot with an integer and a float property. Property Widgets

Symbol and Image Buttons

Nuklear has several stock symbols for symbol buttons. Furthermore one can register a texture to be used in an image button. Each button method returns true if the button was clicked.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.nuklear ; ...
              NkImage]
             ; ...
             [org.lwjgl.stb ; ...
              STBImage]))

; ...

(def download-icon (NkImage/create))
(def w (int-array 1))
(def h (int-array 1))
(def c (int-array 1))
(def buffer (STBImage/stbi_load "download.png" w h c 4))
(def download-tex (GL11/glGenTextures))
(GL11/glBindTexture GL11/GL_TEXTURE_2D download-tex)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MIN_FILTER
                      GL11/GL_LINEAR)
(GL11/glTexParameteri GL11/GL_TEXTURE_2D GL11/GL_TEXTURE_MAG_FILTER
                      GL11/GL_LINEAR)
(GL11/glTexImage2D GL11/GL_TEXTURE_2D 0 GL11/GL_RGBA8 (aget w 0) (aget h 0)
                   0 GL11/GL_RGBA GL11/GL_UNSIGNED_BYTE buffer)
(GL11/glBindTexture GL11/GL_TEXTURE_2D 0)
(def handle (NkHandle/create))
(.id handle download-tex)
(.handle download-icon handle)

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 32 14)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_RECT_SOLID)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_RECT_OUTLINE)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_TRIANGLE_UP)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_TRIANGLE_DOWN)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_TRIANGLE_LEFT)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_TRIANGLE_RIGHT)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_CIRCLE_SOLID)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_CIRCLE_OUTLINE)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_MAX)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_X)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_PLUS)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_MINUS)
         (Nuklear/nk_button_symbol context Nuklear/NK_SYMBOL_UNDERSCORE)
         (if (Nuklear/nk_button_image context download-icon)
           (println "Download"))
         ; ...
         ))

; ...

(GL11/glDeleteTextures download-tex)

; ...

Here is a screenshot showing the buttons instantiated in the code above. Symbol and Image Buttons

Combo Boxes

A combo box lets you choose an option using a drop down menu. It is even possible to have combo boxes with multiple columns.

; ...

(def combo-size (NkVec2/create))
(.x combo-size 320)
(.y combo-size 120)

; ...

(def combo-items (mapv #(str "test" (inc %)) (range 10)))
(def selected (atom (first combo-items)))

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 32 1)
         (when (Nuklear/nk_combo_begin_label context @selected combo-size)
           (Nuklear/nk_layout_row_dynamic context 32 1)
           (doseq [item combo-items]
                  (if (Nuklear/nk_combo_item_text context item
                                                  Nuklear/NK_TEXT_LEFT)
                    (reset! selected item)))
           (Nuklear/nk_combo_end context))
         ; ...
         ))

; ...

The combo box in the following screenshot uses one column. Combo Box

Drawing Custom Widgets

It is possible to use draw commands to draw a custom widget. There are also methods for checking if the mouse is hovering over the widget or if the mouse was clicked.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.nuklear ; ...
              NkColor]
             ; ...
             ))

; ...

(def rgb (NkColor/malloc stack))

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (let [canvas (Nuklear/nk_window_get_canvas context)]
           (Nuklear/nk_layout_row_dynamic context 120 1)
           (if (Nuklear/nk_widget_is_mouse_clicked context
                                                   Nuklear/NK_BUTTON_LEFT)
             (println "Widget clicked"))
           (let [color (if (Nuklear/nk_widget_is_hovered context)
                         (Nuklear/nk_rgb 255 127 127 rgb)
                         (Nuklear/nk_rgb 255 101 101 rgb))]
             (Nuklear/nk_widget rect context)
             (Nuklear/nk_fill_rect canvas rect 2 color)
             (Nuklear/nk_fill_circle canvas
               (Nuklear/nk_rect (+ (.x rect) (- (/ (.w rect) 2) 32))
                                (+ (.y rect) (- (/ (.h rect) 2) 32))
                                64 64 rect)
               (Nuklear/nk_rgb 127 255 127 rgb))))
         ; ...
         ))

; ...

Here we have just drawn a filled rectangle and a filled circle. Custom Widget

Keyboard Input

Finally we need keyboard input. The GLFWDemo.java example uses two GLFW keyboard callbacks and even implements callbacks for the clipboard.

Character Callback

First a character callback is implemented. To test it, we also add an edit field for entering text.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.glfw ; ...
              GLFWCharCallbackI]
             [org.lwjgl.nuklear ; ...
              NkPluginFilter NkPluginFilterI]
             ; ...
             ))

; ...

(GLFW/glfwSetCharCallback
  window
  (reify GLFWCharCallbackI
         (invoke [this window codepoint]
           (Nuklear/nk_input_unicode context codepoint))))

; ...

(def text (BufferUtils/createByteBuffer 256))
(def text-len (int-array [0]))
(def text-filter
  (NkPluginFilter/create
    (reify NkPluginFilterI
           (invoke [this edit unicode]
             (Nuklear/nnk_filter_ascii edit unicode)))))

; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_edit_string context Nuklear/NK_EDIT_FIELD text text-len
                                 256 text-filter)
         ; ...
         ))

; ...

The following screenshot shows the edit field with some text entered. Edit Field

Control Characters

To get control characters working, the second GLFW callback is implemented.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.glfw ; ...
              GLFWKeyCallbackI]
             ; ...
             ))

; ...

(GLFW/glfwSetKeyCallback
  window
  (reify GLFWKeyCallbackI
    (invoke [this window k scancode action mods]
      (let [press (= action GLFW/GLFW_PRESS)]
        (cond
          (= k GLFW/GLFW_KEY_ESCAPE)
            (GLFW/glfwSetWindowShouldClose window true)
          (= k GLFW/GLFW_KEY_DELETE)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_DEL press)
          (= k GLFW/GLFW_KEY_ENTER)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_ENTER press)
          (= k GLFW/GLFW_KEY_TAB)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_TAB press)
          (= k GLFW/GLFW_KEY_BACKSPACE)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_BACKSPACE press)
          (= k GLFW/GLFW_KEY_UP)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_UP press)
          (= k GLFW/GLFW_KEY_DOWN)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_DOWN press)
          (= k GLFW/GLFW_KEY_LEFT)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_LEFT press)
          (= k GLFW/GLFW_KEY_RIGHT)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_RIGHT press)
          (= k GLFW/GLFW_KEY_HOME)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_START press)
          (= k GLFW/GLFW_KEY_END)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_END press)
          (= k GLFW/GLFW_KEY_LEFT_SHIFT)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_SHIFT press)
          (= k GLFW/GLFW_KEY_RIGHT_SHIFT)
            (Nuklear/nk_input_key context Nuklear/NK_KEY_SHIFT press))))))

; ...

Now it is possible to move the cursor in the text box and also delete characters.

Clipboard and other Control Key Combinations

Finally one can implement some Control key combinations. Except for undo and redo I managed to get the keyboard combinations from the GLFWDemo.java example to work. We also implement the clipboard integration.

(ns nukleartest
    (:import ; ...
             [org.lwjgl.nuklear ; ...
              NkPluginCopyI NkPluginPasteI]
             ; ...
             ))

; ...

(GLFW/glfwSetKeyCallback
  window
  (reify GLFWKeyCallbackI
    (invoke [this window k scancode action mods]
      (let [press (= action GLFW/GLFW_PRESS)]
        ; ...
        (when (or (= mods GLFW/GLFW_MOD_CONTROL) (not press))
          (cond
            (= k GLFW/GLFW_KEY_C)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_COPY press)
            (= k GLFW/GLFW_KEY_P)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_PASTE press)
            (= k GLFW/GLFW_KEY_X)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_CUT press)
            (= k GLFW/GLFW_KEY_Z)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_UNDO press)
            (= k GLFW/GLFW_KEY_R)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_REDO press)
            (= k GLFW/GLFW_KEY_LEFT)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_WORD_LEFT
                                    press)
            (= k GLFW/GLFW_KEY_RIGHT)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_WORD_RIGHT
                                    press)
            (= k GLFW/GLFW_KEY_B)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_LINE_START
                                    press)
            (= k GLFW/GLFW_KEY_E)
              (Nuklear/nk_input_key context Nuklear/NK_KEY_TEXT_LINE_END
                                    press)))))))

; ...

(.copy (.clip context)
       (reify NkPluginCopyI
              (invoke [this handle text len]
                (if (not= len 0)
                  (let [stack  (MemoryStack/stackPush)
                        string (.malloc stack (inc len))]
                    (MemoryUtil/memCopy text (MemoryUtil/memAddress string)
                                        len)
                    (.put string len (byte 0))
                    (GLFW/glfwSetClipboardString window string))))))

(.paste (.clip context)
        (reify NkPluginPasteI
               (invoke [this handle edit]
                 (let [text (GLFW/nglfwGetClipboardString window)]
                   (if (not= text 0)
                     (Nuklear/nnk_textedit_paste edit text
                       (Nuklear/nnk_strlen text)))))))

; ...

The following screenshot shows the text edit field with some text selected to copy to the clipboard. Selecting Text

Styling

You can get Nuklear styles from Nuklear/demo/common/style.c. My favourite is the dark theme. The style is set by populating a color table and then using nk_style_from_table to overwrite the style.

; ...

(def style-table (NkColor/malloc Nuklear/NK_COLOR_COUNT))
(.put style-table Nuklear/NK_COLOR_TEXT (Nuklear/nk_rgb 210 210 210 rgb))
(.put style-table Nuklear/NK_COLOR_WINDOW (Nuklear/nk_rgb 57 67 71 rgb))
(.put style-table Nuklear/NK_COLOR_HEADER (Nuklear/nk_rgb 51 51 56 rgb))
(.put style-table Nuklear/NK_COLOR_BORDER (Nuklear/nk_rgb 46 46 46 rgb))
(.put style-table Nuklear/NK_COLOR_BUTTON (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_BUTTON_HOVER (Nuklear/nk_rgb 58 93 121 rgb))
(.put style-table Nuklear/NK_COLOR_BUTTON_ACTIVE (Nuklear/nk_rgb 63 98 126 rgb))
(.put style-table Nuklear/NK_COLOR_TOGGLE (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_TOGGLE_HOVER (Nuklear/nk_rgb 45 53 56 rgb))
(.put style-table Nuklear/NK_COLOR_TOGGLE_CURSOR (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_SELECT (Nuklear/nk_rgb 57 67 61 rgb))
(.put style-table Nuklear/NK_COLOR_SELECT_ACTIVE (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_SLIDER (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_SLIDER_CURSOR (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_SLIDER_CURSOR_HOVER (Nuklear/nk_rgb 53 88 116 rgb))
(.put style-table Nuklear/NK_COLOR_SLIDER_CURSOR_ACTIVE (Nuklear/nk_rgb 58 93 121 rgb))
(.put style-table Nuklear/NK_COLOR_PROPERTY (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_EDIT (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_EDIT_CURSOR (Nuklear/nk_rgb 210 210 210 rgb))
(.put style-table Nuklear/NK_COLOR_COMBO (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_CHART (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_CHART_COLOR (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_CHART_COLOR_HIGHLIGHT (Nuklear/nk_rgb 255 0 0 rgb))
(.put style-table Nuklear/NK_COLOR_SCROLLBAR (Nuklear/nk_rgb 50 58 61 rgb))
(.put style-table Nuklear/NK_COLOR_SCROLLBAR_CURSOR (Nuklear/nk_rgb 48 83 111 rgb))
(.put style-table Nuklear/NK_COLOR_SCROLLBAR_CURSOR_HOVER (Nuklear/nk_rgb 53 88 116 rgb))
(.put style-table Nuklear/NK_COLOR_SCROLLBAR_CURSOR_ACTIVE (Nuklear/nk_rgb 58 93 121 rgb))
(.put style-table Nuklear/NK_COLOR_TAB_HEADER (Nuklear/nk_rgb 48 83 111 rgb))
(Nuklear/nk_style_from_table context style-table)

; ...

And now the window looks like this: Nuklear Dark Theme

More References

This article is already very long, so I will close here. There are still more things to explore. You can check out the LWJGL Nuklear package documentation. Also I can recommend to have a look of the Nuklear Usage Guide and the Nuklear Function Reference by The Coding Fox.

Enjoy!

Updates

Keyboard Repeat Events

Here is a fix to get repeat keypress events for control characters working:

(GLFW/glfwSetKeyCallback
  window
  (reify GLFWKeyCallbackI
         (invoke [this window k scancode action mods]
           (let [press (or (= action GLFW/GLFW_PRESS)
                           (= action GLFW/GLFW_REPEAT))]
             ; ...
             ))))

Polygon Visualisation

If you enable OpenGL polygon mode and clear the image, you can see the polygons Nuklear is creating.

Nuklear polygons

Grouping Widgets

One can group widgets. The group even gets a scrollbar if the content is bigger than the allocated region. The group can also have a title.

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 86 1)
         (Nuklear/nk_group_begin_titled context "settings" "Settings"
           (bit-or Nuklear/NK_WINDOW_TITLE Nuklear/NK_WINDOW_BORDER))
         (Nuklear/nk_layout_row_dynamic context 32 1)
         (Nuklear/nk_property_int context "Compression:" 0 compression 100 10
                                  (float 1))
         (Nuklear/nk_property_float context "Quality:" (float 0.0) quality
                                    (float 10.0) (float 1.0) (float 0.01))
         (Nuklear/nk_group_end context)
         ; ...
         ))

; ...

The screenshot shows the compression and quality property widgets grouped together. Widget Group

Slider

Sliders are an alternative to property widgets. They display the value using a circle instead of using a textual representation.

; ...
(def slider (BufferUtils/createIntBuffer 1))
(.put slider 0 50)
; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_slider_int context 0 slider 100 1)
         ; ...
         ))

; ...

Here is a screenshot showing a slider. Slider Widget

Nested Layouts

Nuklear does not seem to support nested layouts. However as shown by Komari Spaghetti one can use groups for nesting layouts in Nuklear. Basically you just need to set window padding to zero temporarily and disable the scroll bars. In the following sample there are five buttons using two columns with different button sizes.

; ...
(def no-padding (NkVec2/create))
(.x no-padding 0)
(.y no-padding 0)
; ...

(while (not (GLFW/glfwWindowShouldClose window))
       ; ...
       (when (Nuklear/nk_begin context "Nuklear Example"
                               (Nuklear/nk_rect 0 0 width height rect) 0)
         ; ...
         (Nuklear/nk_layout_row_dynamic context 100 2)
         (Nuklear/nk_style_push_vec2 context
           (.group_padding (.window (.style context))) no-padding)
         (Nuklear/nk_group_begin context "buttons-1"
                                 Nuklear/NK_WINDOW_NO_SCROLLBAR)
         (Nuklear/nk_layout_row_dynamic context 26 1)
         (Nuklear/nk_button_label context "Button A")
         (Nuklear/nk_button_label context "Button B")
         (Nuklear/nk_button_label context "Button C")
         (Nuklear/nk_group_end context)
         (Nuklear/nk_group_begin context "buttons-2"
                                 Nuklear/NK_WINDOW_NO_SCROLLBAR)
         (Nuklear/nk_layout_row_dynamic context 41 1)
         (Nuklear/nk_button_label context "Button D")
         (Nuklear/nk_button_label context "Button E")
         (Nuklear/nk_group_end context)
         (Nuklear/nk_style_pop_vec2 context)
         ; ...
         ))

; ...

The screenshot shows the layout achieved in this case. Nuklear Layout

Type hints

In order to improve performance, one can use Clojure type hints. This is especially effective when applied to the implementations of width and query method or the NkUserFont object. width gets called for each string and query even gets called for each character in the GUI. One can enable reflection warnings in order to find where type hints are needed to improve performance.

(set! *warn-on-reflection* true)
; ...
  (STBTruetype/stbtt_GetCodepointHMetrics ^STBTTFontinfo fontinfo
    (.get unicode 0) advance nil)
; ...
  (STBTruetype/stbtt_GetPackedQuad ^STBTTPackedchar$Buffer cdata
    ^long bitmap-w ^long bitmap-h (- codepoint 32) x y q false)
  (STBTruetype/stbtt_GetCodepointHMetrics ^STBTTFontinfo fontinfo
    codepoint advance nil)
; ...

By fixing all reflection warnings (see v1.1), I managed to reduce the CPU load of the GUI prototype from 50.8% down to 9.6%!

Getting started with nREPL server and REPL-y client

nREPL

If you are using Clojure, you might be interested in nREPL which lets you connect a REPL terminal to a running Clojure program. In the following howto I am setting up a small nREPL demo using the nREPL server and the REPL-y nREPL client.

First I set up aliases for the server and client in $HOME/.clojure/deps.edn as follows:

{:aliases {:nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.1.0"}}}
           :reply {:extra-deps {reply/reply {:mvn/version "0.5.1"}}
                   :main-opts ["-m" "reply.main" "--attach" "7888"]}}}

Now I need a small demo program to test things out. First I create $HOME/Documents/repltest/deps.edn which just specifies the Clojure version.

{:deps {org.clojure/clojure {:mvn/version "1.11.1"}}
 :paths ["src"]}

The following program then displays a counter which gets increased once per second. Furthermore it starts an nREPL server on port 7888. The program goes into the file $HOME/Documents/repltest/src/repltest/core.clj.

(ns repltest.core
  "nREPL demo"
  (:gen-class))

(require '[nrepl.server :refer [start-server stop-server]])
(defonce server (start-server :port 7888))

(def t (atom 0))

(defn display
  [value]
  (println value))

(defn -main
  "nREPL demo"
  [& _args]
  (while true
         (display (swap! t inc))
         (Thread/sleep 1000))
  (System/exit 0))

Now one can run the program using clj -M:nrepl -m repltest.core. The program will print out consecutive numbers as follows:

1
2
3
4
.
.
.

Now you need to open a second terminal for the nREPL client. You run the network client using clojure -M:reply. The important thing which took me some time to find out is that you need to then switch to your applications namespace as follows:

user=> (ns repltest.core)

Now you can easily access the variables of the main program:

repltest.core=> @t
42

You can also modify the value while the main program is still running:

repltest.core=> (swap! t - 10)
32

You should see the counter decrease in the application’s output.

You can even redefine the display methods using the nREPL client. I.e. you can do interactive development.

repltest.core=> (defn display [value] (println "value =" value))

The program output will now be modified as follows:

value = 32
value = 33
value = 34
.
.
.

See github.com/wedesoft/repltest for the demo code.

Enjoy!

Omikron Basic

The first program I wrote as a kid was an Omikron Basic program on an Atari 1040ST. I wrote a program to print something and later I tried out a for-loop. It took me a long time to figure out that there was an EDIT mode with automatic line numbering. Using Hatari I recreated a short video of this:

Here is the download link for the Omikron Basic STX disk image.

Flight Simulator 2 on Atari ST

As a kid I got the joy of playing Flight Simulator 2 on my dad’s Atari 1040ST. The Atari had a 640x400 monochrome monitor.

I noticed that Debian 12 comes with the Hatari Atari emulator. Furthermore one can download a disk image of Flight Simulator 2 at atarimania.com. Then it was just a matter of unzipping the file and specifying the disk image on the Hatari command line.

sudo apt install hatari
unzip flight_simulator_2.zip
hatari --mono --disk-a "Flight Simulator 2.stx"

I took a video of the initial part of the demo mode here:

Enjoy!

Update: You also need to download a TOS image for Atari ST and copy it to /usr/share/hatari/tos.img.

Specifying Clojure function schemas with Malli

Clojure is a dynamically typed language which means that functions can be called with arbitrary values. If the function cannot operate on the values (or if it calls a function which cannot operate on a value or part of a value) a runtime exception is thrown. Even worse, a function could exhibit undefined behaviour for the values provided.

Statically typed languages are often considered safer because the types of values are known at compile time so that the validity of values can be checked. However statically typed implementations tend to become inflexible. Type checking at compile time requires the types of the values held by variables to be defined at compile time. When trying to keep the system both modular and extensible, the developer is forced to come up with complicated type hierarchies and possibly even generic types.

There is a promising solution to this and this is using pre- and post-conditions (contracts) for functions. In particular there is Malli which allows you to specify schemas for each function using a concise syntax. Malli schemas can be defined, reused, and composed like types. Note that a value can fulfill multiple schemas (similar to Rust traits), however implementations and schemas exist independently from each other. This allows schemas to be more specific than static types will ever be (e.g. you can have a schema for lists with three or more elements).

Malli is optimized for good runtime performance. If performance is still a concern, one can leave schema validation disabled in production for some or all namespaces.

Here follows a short introduction to using Malli for function pre- and post-conditions:

You can use the Malli provider module to suggest possible schemas matching a list of values.

(require '[malli.provider :as mp])
(mp/provide [1])
; :int
(mp/provide [1 nil])
; [:maybe :int]
(mp/provide [[1 2 3]])
; [:vector :int]

The resulting schemas can be used to validate values.

(require '[malli.core :as m])
(m/validate [:vector :int] [1 2 3])
; true
(m/validate [:vector :int] [1 2 nil])
; false

Schemas are values which can be reused and composed.

(def N (m/schema [:int {:min 1}]))
(m/validate [:vector N] [0 1 2])
; false
(m/validate [:vector N] [1 2 3])
; true

It is possible to add predicate functions to schemas.

(def ordered-vector (m/schema [:and [:vector :int] [:fn (fn [v] (apply < v))]]))
(m/validate ordered-vector [2 5 3])
; false
(m/validate ordered-vector [2 3 5])
; true

Most importantly one can add pre- and post-conditions (function schemas) to functions.

(defn sqr
  {:malli/schema [:=> [:cat :double] :double]}
  [x]
  (* x x))

By default function schemas are ignored until the schemas are collected and the functions are instrumented. Note that here I am replacing the default exception thrower with a more human-readable one.

(require '[malli.instrument :as mi])
(require '[malli.dev.pretty :as pretty])
(mi/collect!)
(mi/instrument! {:report (pretty/thrower)})
(sqr 3.0)
; 9.0
(sqr "text")
; -- Schema Error ---------------------------------------------------------------- NO_SOURCE_FILE:1 --
;
; Invalid function arguments:
;
;   ["text"]
;
; Function Var:
;
;   user/sqr
;
; Input Schema:
;
;   [:cat :double]
;
; Errors:
;
;   {:in [0], :message "should be a double", :path [0], :schema :double, :value "text"}
;
; More information:
;
;   https://cljdoc.org/d/metosin/malli/CURRENT/doc/function-schemas
;
; ----------------------------------------------------------------------------------------------------

If you want to collect the schemas from a module (e.g. for testing purposes), you can specify the namespace as follows.

(mi/collect! {:ns ['the.module]})

You can also parse code to infer a general schema for a function header.

(require '[malli.destructure :as md])
(def infer (comp :schema md/parse))
(infer '[x])
; [:cat :any]

If you have a multiple arity function, you can use :function to specify the different alternatives.

(defn f
  {:malli/schema [:function [:=> [:cat :double] :double]
                            [:=> [:cat :double :double] :double]]}
  ([x] x)
  ([x y] (+ x y)))
(mi/collect!)
(mi/instrument! {:report (pretty/thrower)})
(f 2.0 3.0)
; 5.0
(f "test")
; ... error ...

There is also support for sequence schemas which allows a more compact schema in this case.

(defn f
  {:malli/schema [:=> [:cat :double [:? :double]] :double]}
  ([x] x)
  ([x y] (+ x y)))

Finally here is an example for a method accepting keyword arguments which took me some time to figure out.

(defn g
  {:malli/schema [:=> [:cat [:= :x] :double [:= :y] :double] :double]}
  [& {:keys [x y]}]
  (+ x y))
(mi/collect!)
(mi/instrument! {:report (pretty/thrower)})
(g :x 2.0 :y 3.0)
; 5.0
(g :x 2.0 :y "test")
; ... error ...

The only problem I encountered so far is that instrumentation clashes with Clojure type hints. I guess the solution is to instead provide the type hints at the call site instead where necessary for performance reasons.

Enjoy!

Updates:

You can also define recursive schemas by using a local registry:

(require '[malli.core :as m])
(def nested-vector
  (m/schema [:schema {:registry {::node [:or :int [:vector [:ref ::node]]]}}
                     [:ref ::node]]))
(m/validate nested-vector [[1 2] 3])
; true
(m/validate nested-vector [[1 :k] 3])
; false

One can use m/=> to add a Malli schema to a function. This comes in handy when you need to add a schema to a Clojure multimethod.

(require '[malli.core :as m])
(require '[malli.instrument :as mi])
(require '[malli.dev.pretty :as pretty])
(defmulti factorial identity)
(m/=> factorial [:=> [:cat integer?] integer?])
(defmethod factorial 0 [_]  1N)
(defmethod factorial :default [num]
   (* num (factorial (dec num))))
(mi/instrument! {:report (pretty/thrower)})
(factorial 10)
; 3628800N
(factorial 1.5)
; ... error ...