Nuklear icon indicating copy to clipboard operation
Nuklear copied to clipboard

[BUG] `nk_label_wrap` eats the last character at line breaks (`nk_text_clamp` is not completely correct)

Open PavelSharp opened this issue 3 months ago • 1 comments

Hello, I’ve found a bug in Nuklear related to nk_label_wrap (and other wrapped text widgets). When rendering wrapped text, the last character of a line is incorrectly dropped due to an off-by-one logic issue in nk_text_clamp.

Expected behavior All characters in the input string should be visible, with line breaks applied when the text exceeds the available width.

Actual behavior The last character on each wrapped line is missing (see figure). Image

Minimal Reproducible Example The following standalone code (C99 + GLFW3 + GLAD + Nuklear) demonstrates the issue:

Source code
//2025.09.10
//BUG: Last character of each wrapped line disappears (issue in nk_text_clamp)
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <glad/gl.h>
#include <GLFW/glfw3.h>

#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_IMPLEMENTATION
#define NK_GLFW_GL3_IMPLEMENTATION
#include "nuklear/nuklear.h"
#include "nuklear/nuklear_glfw_gl3.h"

#define MAX_VERTEX_BUFFER 512 * 1024
#define MAX_ELEMENT_BUFFER 128 * 1024

typedef struct {
    GLFWwindow* window;
    struct nk_context* nk_ctx;
    struct nk_glfw nk_glfw;
} app_state;


static void app_init_glfw_window(app_state* st, int w, int h) {
    assert(glfwInit());

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    st->window = glfwCreateWindow(w, h, "Test App", NULL, NULL);
    assert(st->window);
    
    glfwSetWindowPos(st->window, 100, 100);
    glfwMakeContextCurrent(st->window);

    assert(gladLoadGL(glfwGetProcAddress));
}

static void app_nuklear_init(app_state* st) {
    st->nk_ctx = nk_glfw3_init(&st->nk_glfw, st->window, NK_GLFW3_INSTALL_CALLBACKS);

    struct nk_font_atlas* atlas;
    nk_glfw3_font_stash_begin(&st->nk_glfw, &atlas);
    nk_glfw3_font_stash_end(&st->nk_glfw);
}

static void app_init(app_state* st) {
    app_init_glfw_window(st, 800, 600);
    app_nuklear_init(st);
}

static inline int app_window_open(app_state* st) {
    return !glfwWindowShouldClose(st->window);
}

static void app_ui_frame(app_state* st) {
    struct nk_context* ctx = st->nk_ctx;

    nk_glfw3_new_frame(&st->nk_glfw);

    nk_begin(ctx, "ABC", nk_rect(10, 10, 100, 200), NK_WINDOW_NOT_INTERACTIVE | NK_WINDOW_NO_SCROLLBAR);
    {
        nk_layout_row_dynamic(ctx, 100, 1);
        struct nk_rect r = nk_widget_bounds(ctx);
        nk_fill_rect(&ctx->current->buffer, r, 0, nk_rgb(110,70,70));
        nk_label_wrap(ctx, "0123456789_0123456789_0123456789_0123456789_0123456789_0123456789");
    }
    nk_end(ctx);

    nk_glfw3_render(&st->nk_glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER, MAX_ELEMENT_BUFFER);
}

static void app_frame(app_state* st) {
    glfwPollEvents();
    int width, height;
    glfwGetFramebufferSize(st->window, &width, &height);

    glViewport(0, 0, width, height);
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    app_ui_frame(st);
    glfwSwapBuffers(st->window);
}

static void app_free(app_state* st) {
    nk_glfw3_shutdown(&st->nk_glfw);
    glfwDestroyWindow(st->window);
    glfwTerminate();
}

int main(void) {
    static app_state st = { 0 };
    app_init(&st);
    while (app_window_open(&st)) {
        app_frame(&st);
    }
    app_free(&st);
    return 0;
}

PavelSharp avatar Sep 10 '25 19:09 PavelSharp

After a while, I found out that there are actually a lot more problems with the nk_text_camp function (see figure).

Image
Source code
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <glad/gl.h>
#include <GLFW/glfw3.h>

#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_IMPLEMENTATION
#define NK_GLFW_GL3_IMPLEMENTATION
#include "nuklear/nuklear.h"
#include "nuklear/nuklear_glfw_gl3.h"

#define MAX_VERTEX_BUFFER 512 * 1024
#define MAX_ELEMENT_BUFFER 128 * 1024

typedef struct {
    GLFWwindow* window;
    struct nk_context* nk_ctx;
    struct nk_glfw nk_glfw;
} app_state;


static void app_init_glfw_window(app_state* st, int w, int h) {
    assert(glfwInit());

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    st->window = glfwCreateWindow(w, h, "Test App", NULL, NULL);
    assert(st->window);
    
    glfwSetWindowPos(st->window, 100, 100);
    glfwMakeContextCurrent(st->window);

    assert(gladLoadGL(glfwGetProcAddress));
}

static void app_nuklear_init(app_state* st) {
    st->nk_ctx = nk_glfw3_init(&st->nk_glfw, st->window, NK_GLFW3_INSTALL_CALLBACKS);
    struct nk_font_atlas* atlas;
    nk_glfw3_font_stash_begin(&st->nk_glfw, &atlas);
    nk_glfw3_font_stash_end(&st->nk_glfw);
}

static void app_init(app_state* st) {
    app_init_glfw_window(st, 300, 129);
    app_nuklear_init(st);
}

static inline int app_window_open(app_state* st) {
    return !glfwWindowShouldClose(st->window);
}

static const char* test[] = {
    "012 345",
    "0123456789_0123456789_0123456789_0123456789_0123456789_0123456789",
    "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789",
    "  0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 "
};

static void app_ui_frame(app_state* st) {
    struct nk_context* ctx = st->nk_ctx;
    int width, height;
    glfwGetFramebufferSize(st->window, &width, &height);

    nk_glfw3_new_frame(&st->nk_glfw);

    nk_begin(ctx, "ABC", nk_rect(10, 10, width-20, 109), NK_WINDOW_NOT_INTERACTIVE | NK_WINDOW_NO_SCROLLBAR);
    {
        const int items = sizeof(test) / sizeof(char*);
        nk_layout_row_dynamic(ctx, 100, items);
        for (int i = 0;i < items;i++) {
            struct nk_rect r = nk_widget_bounds(ctx);
            nk_fill_rect(&ctx->current->buffer, r, 0, nk_rgb(110, 70, 70));
            nk_label_wrap(ctx, test[i]);
        }
    }
    nk_end(ctx);

    nk_glfw3_render(&st->nk_glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER, MAX_ELEMENT_BUFFER);
}

static void app_frame(app_state* st) {
    glfwPollEvents();
    int width, height;
    glfwGetFramebufferSize(st->window, &width, &height);

    glViewport(0, 0, width, height);
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    app_ui_frame(st);
    glfwSwapBuffers(st->window);
}

static void app_free(app_state* st) {
    nk_glfw3_shutdown(&st->nk_glfw);
    glfwDestroyWindow(st->window);
    glfwTerminate();
}

int main(void) {
    static app_state st = { 0 };
    app_init(&st);
    while (app_window_open(&st)) {
        app_frame(&st);
    }
    app_free(&st);
    return 0;
}

PavelSharp avatar Sep 11 '25 19:09 PavelSharp