Nuklear
Minimal-state, immediate-mode graphical user interface toolkit written in ANSI C.
 
Loading...
Searching...
No Matches
Drawing

This library was designed to be render backend agnostic so it does not draw anything to screen directly.

This library was designed to be render backend agnostic so it does not draw anything to screen.

Instead all drawn shapes, widgets are made of, are buffered into memory and make up a command queue. Each frame therefore fills the command buffer with draw commands that then need to be executed by the user and his own render backend. After that the command buffer needs to be cleared and a new frame can be started. It is probably important to note that the command buffer is the main drawing API and the optional vertex buffer API only takes this format and converts it into a hardware accessible format.

Usage

To draw all draw commands accumulated over a frame you need your own render backend able to draw a number of 2D primitives. This includes at least filled and stroked rectangles, circles, text, lines, triangles and scissors. As soon as this criterion is met you can iterate over each draw command and execute each draw command in a interpreter like fashion:

const struct nk_command *cmd = 0;
nk_foreach(cmd, &ctx) {
switch (cmd->type) {
case NK_COMMAND_LINE:
your_draw_line_function(...)
break;
case NK_COMMAND_RECT
your_draw_rect_function(...)
break;
case //...:
//[...]
}
}
command base and header of every command inside the buffer
Definition nuklear.h:4510

In program flow context draw commands need to be executed after input has been gathered and the complete UI with windows and their contained widgets have been executed and before calling nk_clear which frees all previously allocated draw commands.

struct nk_context ctx;
nk_init_xxx(&ctx, ...);
while (1) {
Event evt;
nk_input_begin(&ctx);
while (GetEvent(&evt)) {
if (evt.type == MOUSE_MOVE)
nk_input_motion(&ctx, evt.motion.x, evt.motion.y);
else if (evt.type == [...]) {
[...]
}
}
nk_input_end(&ctx);
//
// [...]
//
const struct nk_command *cmd = 0;
nk_foreach(cmd, &ctx) {
switch (cmd->type) {
case NK_COMMAND_LINE:
your_draw_line_function(...)
break;
case NK_COMMAND_RECT
your_draw_rect_function(...)
break;
case ...:
// [...]
}
nk_clear(&ctx);
}
nk_free(&ctx);

You probably noticed that you have to draw all of the UI each frame which is quite wasteful. While the actual UI updating loop is quite fast rendering without actually needing it is not. So there are multiple things you could do.

First is only update on input. This of course is only an option if your application only depends on the UI and does not require any outside calculations. If you actually only update on input make sure to update the UI two times each frame and call nk_clear directly after the first pass and only draw in the second pass. In addition it is recommended to also add additional timers to make sure the UI is not drawn more than a fixed number of frames per second.

struct nk_context ctx;
nk_init_xxx(&ctx, ...);
while (1) {
// [...wait for input ]
// [...do two UI passes ...]
do_ui(...)
nk_clear(&ctx);
do_ui(...)
//
// draw
const struct nk_command *cmd = 0;
nk_foreach(cmd, &ctx) {
switch (cmd->type) {
case NK_COMMAND_LINE:
your_draw_line_function(...)
break;
case NK_COMMAND_RECT
your_draw_rect_function(...)
break;
case ...:
//[...]
}
nk_clear(&ctx);
}
nk_free(&ctx);

The second probably more applicable trick is to only draw if anything changed. It is not really useful for applications with continuous draw loop but quite useful for desktop applications. To actually get nuklear to only draw on changes you first have to define NK_ZERO_COMMAND_MEMORY and allocate a memory buffer that will store each unique drawing output. After each frame you compare the draw command memory inside the library with your allocated buffer by memcmp. If memcmp detects differences you have to copy the command buffer into the allocated buffer and then draw like usual (this example uses fixed memory but you could use dynamically allocated memory).

//[... other defines ...]
#define NK_ZERO_COMMAND_MEMORY
#include "nuklear.h"
//
// setup context
struct nk_context ctx;
void *last = calloc(1,64*1024);
void *buf = calloc(1,64*1024);
nk_init_fixed(&ctx, buf, 64*1024);
//
// loop
while (1) {
// [...input...]
// [...ui...]
void *cmds = nk_buffer_memory(&ctx.memory);
if (memcmp(cmds, last, ctx.memory.allocated)) {
memcpy(last,cmds,ctx.memory.allocated);
const struct nk_command *cmd = 0;
nk_foreach(cmd, &ctx) {
switch (cmd->type) {
case NK_COMMAND_LINE:
your_draw_line_function(...)
break;
case NK_COMMAND_RECT
your_draw_rect_function(...)
break;
case ...:
// [...]
}
}
}
nk_clear(&ctx);
}
nk_free(&ctx);

Finally while using draw commands makes sense for higher abstracted platforms like X11 and Win32 or drawing libraries it is often desirable to use graphics hardware directly. Therefore it is possible to just define NK_INCLUDE_VERTEX_BUFFER_OUTPUT which includes optional vertex output. To access the vertex output you first have to convert all draw commands into vertices by calling nk_convert which takes in your preferred vertex format. After successfully converting all draw commands just iterate over and execute all vertex draw commands:

// fill configuration
struct your_vertex
{
float pos[2]; // important to keep it to 2 floats
float uv[2];
unsigned char col[4];
};
struct nk_convert_config cfg = {};
static const struct nk_draw_vertex_layout_element vertex_layout[] = {
{NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct your_vertex, pos)},
{NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct your_vertex, uv)},
{NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct your_vertex, col)},
{NK_VERTEX_LAYOUT_END}
};
cfg.shape_AA = NK_ANTI_ALIASING_ON;
cfg.line_AA = NK_ANTI_ALIASING_ON;
cfg.vertex_layout = vertex_layout;
cfg.vertex_size = sizeof(struct your_vertex);
cfg.vertex_alignment = NK_ALIGNOF(struct your_vertex);
cfg.global_alpha = 1.0f;
cfg.tex_null = dev->tex_null;
//
// setup buffers and convert
struct nk_buffer cmds, verts, idx;
nk_buffer_init_default(&cmds);
nk_buffer_init_default(&verts);
nk_buffer_init_default(&idx);
nk_convert(&ctx, &cmds, &verts, &idx, &cfg);
//
// draw
nk_draw_foreach(cmd, &ctx, &cmds) {
if (!cmd->elem_count) continue;
//[...]
}
nk_buffer_free(&cms);
nk_buffer_free(&verts);
nk_buffer_free(&idx);
enum nk_anti_aliasing shape_AA
!< line anti-aliasing flag can be turned off if you are tight on memory
Definition nuklear.h:1020
enum nk_anti_aliasing line_AA
!< global alpha value
Definition nuklear.h:1019
nk_size vertex_alignment
!< sizeof one vertex for vertex packing
Definition nuklear.h:1027
nk_size vertex_size
!< describes the vertex output format and packing
Definition nuklear.h:1026
const struct nk_draw_vertex_layout_element * vertex_layout
!< handle to texture with a white pixel for shape drawing
Definition nuklear.h:1025
unsigned arc_segment_count
!< number of segments used for circles: default to 22
Definition nuklear.h:1022
unsigned circle_segment_count
!< shape anti-aliasing flag can be turned off if you are tight on memory
Definition nuklear.h:1021
unsigned curve_segment_count
!< number of segments used for arcs: default to 22
Definition nuklear.h:1023
struct nk_draw_null_texture tex_null
!< number of segments used for curves: default to 22
Definition nuklear.h:1024

Reference

Function Description
nk__begin Returns the first draw command in the context draw command list to be drawn
nk__next Increments the draw command iterator to the next command inside the context draw command list
nk_foreach Iterates over each draw command inside the context draw command list
nk_convert Converts from the abstract draw commands list into a hardware accessible vertex format
nk_draw_begin Returns the first vertex command in the context vertex draw list to be executed
nk__draw_next Increments the vertex command iterator to the next command inside the context vertex command list
nk__draw_end Returns the end of the vertex draw list
nk_draw_foreach Iterates over each vertex draw command inside the vertex draw list

Instead all drawn shapes, widgets are made of, are buffered into memory and make up a command queue. Each frame therefore fills the command buffer with draw commands that then need to be executed by the user and his own render backend. After that the command buffer needs to be cleared and a new frame can be started. It is probably important to note that the command buffer is the main drawing API and the optional vertex buffer API only takes this format and converts it into a hardware accessible format.

To use the command queue to draw your own widgets you can access the command buffer of each window by calling nk_window_get_canvas after previously having called nk_begin:

void draw_red_rectangle_widget(struct nk_context *ctx)
{
struct nk_command_buffer *canvas;
struct nk_input *input = &ctx->input;
canvas = nk_window_get_canvas(ctx);
struct nk_rect space;
enum nk_widget_layout_states state;
state = nk_widget(&space, ctx);
if (!state) return;
if (state != NK_WIDGET_ROM)
update_your_widget_by_user_input(...);
nk_fill_rect(canvas, space, 0, nk_rgb(255,0,0));
}
if (nk_begin(...)) {
nk_layout_row_dynamic(ctx, 25, 1);
draw_red_rectangle_widget(ctx);
}
nk_end(..)

Important to know if you want to create your own widgets is the nk_widget call. It allocates space on the panel reserved for this widget to be used, but also returns the state of the widget space. If your widget is not seen and does not have to be updated it is '0' and you can just return. If it only has to be drawn the state will be NK_WIDGET_ROM otherwise you can do both update and draw your widget. The reason for separating is to only draw and update what is actually necessary which is crucial for performance.