Immediate-Mode-UI / Nuklear

A single-header ANSI C immediate mode cross-platform GUI library
https://immediate-mode-ui.github.io/Nuklear/
Other
9.36k stars 571 forks source link

(HELP PLEASE!!! Also, potential bugfix supplied.) Getting some strange text clipping behavior when dragging windows around. #646

Open StrikerMan780 opened 6 months ago

StrikerMan780 commented 6 months ago

The text seems to randomly clip for whatever reason, when I drag the window around my screen. Not entirely sure why.

Most of the code is based on the demo.

Link to the video demonstrating the problem: https://shadowmavericks.com/files/ShareX/tfury_2024-05-25_22-41-40.mp4 Alt: https://github.com/Immediate-Mode-UI/Nuklear/assets/1618721/8099e1ff-450a-486f-9282-a82584c83bbd

My code thus far:

#include <string.h>

#define NK_IMPLEMENTATION
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
//#define NK_UINT_DRAW_INDEX
#include "nuklear.h"
#include "GLRender.h"

struct nk_context ctx;
struct nk_user_font uiFont;
struct nk_colorf bg;

struct tfury_gui_vertex
{
    float position[2];
    float uv[2];
    float col[4];
};

typedef struct
{
    SFont *font;
    float scale;
} tfury_uifont_info;

struct nk_convert_config config;
struct nk_buffer cmds, vbuf, ebuf;

static float UI_GetMessageWidth(nk_handle handle, float height, const char *text, int len)
{
    tfury_uifont_info *fontInfo = handle.ptr;
    SFont *font = fontInfo->font;
    if (font == NULL)
        return 0.0;

    float width = TXT_GetMessageWidth((char *)text, font, fontInfo->scale);

    return width;
}

static void UI_GetGlyph(nk_handle handle, float font_height, struct nk_user_font_glyph *glyph, nk_rune codepoint, nk_rune next_codepoint)
{
    tfury_uifont_info *fontInfo = handle.ptr;
    SFont *font = fontInfo->font;
    if (font == NULL)
        return;

    SGlyph scaled = TXT_GetGlyphScaled((char)codepoint, font, fontInfo->scale);

    codepoint -= FIRST_CHAR;
    if (codepoint < 0)
        return;

    glyph->width = scaled.width;
    glyph->height = scaled.height;
    glyph->offset.x = scaled.xoffset;
    glyph->offset.y = scaled.yoffset;
    glyph->xadvance = scaled.advance;
    glyph->uv[0].x = scaled.x / font->texture_x;
    glyph->uv[0].y = scaled.y / font->texture_y;
    glyph->uv[1].x = (scaled.x + font->glyphs[codepoint].width) / font->texture_x;
    glyph->uv[1].y = (scaled.y + font->glyphs[codepoint].height) / font->texture_y;
}

struct nk_user_font UI_LoadUserFont(SFont *font, float scale)
{
    struct nk_user_font userFont;

    // Todo: Add a function to free this!
    tfury_uifont_info *fontInfo = (tfury_uifont_info *)malloc(sizeof(tfury_uifont_info));
    fontInfo->font = font;
    fontInfo->scale = scale;

    userFont.userdata.ptr = fontInfo;
    userFont.height = TXT_GetFontInfo(FONTINFO_LINEHEIGHT, font, scale);
    userFont.width = UI_GetMessageWidth;
    userFont.query = UI_GetGlyph;
    userFont.texture.id = font->texture;

    return userFont;
}

void UI_Init()
{
    uiFont = UI_LoadUserFont(&smallfont, 0.6);
    nk_init_default(&ctx, &uiFont);

    static const struct nk_draw_vertex_layout_element vertex_layout[] = {
            {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct tfury_gui_vertex, position)},
            {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct tfury_gui_vertex, uv)},
            {NK_VERTEX_COLOR, NK_FORMAT_R32G32B32A32_FLOAT, NK_OFFSETOF(struct tfury_gui_vertex, col)},
            {NK_VERTEX_LAYOUT_END}
    };

    memset(&config, 0, sizeof(config));
    config.vertex_layout = vertex_layout;
    config.vertex_size = sizeof(struct tfury_gui_vertex);
    config.vertex_alignment = NK_ALIGNOF(struct tfury_gui_vertex);
    config.tex_null.texture.id = whiteTexture;
    config.tex_null.uv.x = 0.0;
    config.tex_null.uv.y = 0.0;
    config.circle_segment_count = 22;
    config.curve_segment_count = 22;
    config.arc_segment_count = 22;
    config.global_alpha = 1.0f;
    config.shape_AA = NK_ANTI_ALIASING_ON;
    config.line_AA = NK_ANTI_ALIASING_OFF;

    nk_buffer_init_default(&cmds);
}

void UI_Draw()
{
    GLsizei vstr = sizeof(struct tfury_gui_vertex);

    GLint viewport_save[4];
    glGetIntegerv(GL_VIEWPORT, viewport_save);
    glViewport(0, 0, gamePIXX, gamePIXY);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0.0f, gamePIXX, gamePIXY, 0.0f, -1.0f, 1.0f);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    /* GUI */
    if (nk_begin(&ctx, "Demo", nk_rect(50, (gamePIXY/2.0f)-125.0f, 230, 250),
                 NK_WINDOW_BORDER | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                 NK_WINDOW_MINIMIZABLE | NK_WINDOW_TITLE))
    {
        enum
        {
            EASY, HARD
        };
        static int op = EASY;
        static int property = 20;

        nk_layout_row_static(&ctx, 30, 128, 1);
        if (nk_button_label(&ctx, "button"))
            fprintf(stdout, "button pressed\n");
        nk_layout_row_dynamic(&ctx, 30, 2);
        if (nk_option_label(&ctx, "easy", op == EASY)) op = EASY;
        if (nk_option_label(&ctx, "hard", op == HARD)) op = HARD;
        nk_layout_row_dynamic(&ctx, 25, 1);
        nk_property_int(&ctx, "Compression:", 0, &property, 100, 10, 1);

        nk_layout_row_dynamic(&ctx, 20, 1);
        nk_label(&ctx, "background:", NK_TEXT_LEFT);
        nk_layout_row_dynamic(&ctx, 25, 1);
        if (nk_combo_begin_color(&ctx, nk_rgb_cf(bg), nk_vec2(nk_widget_width(&ctx), 400)))
        {
            nk_layout_row_dynamic(&ctx, 120, 1);
            bg = nk_color_picker(&ctx, bg, NK_RGBA);
            nk_layout_row_dynamic(&ctx, 25, 1);
            bg.r = nk_propertyf(&ctx, "#R:", 0, bg.r, 1.0f, 0.01f, 0.005f);
            bg.g = nk_propertyf(&ctx, "#G:", 0, bg.g, 1.0f, 0.01f, 0.005f);
            bg.b = nk_propertyf(&ctx, "#B:", 0, bg.b, 1.0f, 0.01f, 0.005f);
            bg.a = nk_propertyf(&ctx, "#A:", 0, bg.a, 1.0f, 0.01f, 0.005f);
            nk_combo_end(&ctx);
        }
    }
    nk_end(&ctx);

    nk_buffer_init_default(&vbuf);
    nk_buffer_init_default(&ebuf);
    nk_convert(&ctx, &cmds, &vbuf, &ebuf, &config);
    {
        const struct tfury_gui_vertex *vertices = (struct tfury_gui_vertex *)nk_buffer_memory_const(&vbuf);
        glVertexAttribPointer(VERTEXATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, vstr, &vertices[0].position);
        glVertexAttribPointer(VERTEXATTRIB_TEXCOORD0, 2, GL_FLOAT, GL_FALSE, vstr, &vertices[0].uv);
        glVertexAttribPointer(VERTEXATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, vstr, &vertices[0].col);
        glEnableVertexAttribArray(VERTEXATTRIB_POSITION);
        glEnableVertexAttribArray(VERTEXATTRIB_COLOR);
        glEnableVertexAttribArray(VERTEXATTRIB_TEXCOORD0);
    }

    struct nk_vec2 scale;
    scale.x = 1.0f;
    scale.y = 1.0f;

    glEnable(GL_SCISSOR_TEST);
    glEnable(GL_BLEND);
    glDisable(GL_CULL_FACE);
    glDisable(GL_DEPTH_TEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // iterate over and execute each draw command
    const struct nk_draw_command *cmd;
    const nk_draw_index *offset = NULL;
    offset = (const nk_draw_index *)nk_buffer_memory_const(&ebuf);
    nk_draw_foreach(cmd, &ctx, &cmds)
    {
        if (!cmd->elem_count)
            continue;
        glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
        glScissor(
            (GLint)(cmd->clip_rect.x * scale.x),
            (GLint)((gamePIXY - (GLint)(cmd->clip_rect.y + cmd->clip_rect.h)) * scale.y),
            (GLint)(cmd->clip_rect.w * scale.x),
            (GLint)(cmd->clip_rect.h * scale.y));
        glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
        offset += cmd->elem_count;
    }

    nk_clear(&ctx);
    nk_buffer_clear(&cmds);
    nk_buffer_free(&vbuf);
    nk_buffer_free(&ebuf);

    glDisable(GL_SCISSOR_TEST);
    glDisable(GL_BLEND);

    RENDER_SetArrays(ARRAY_MAIN);
    glViewport(viewport_save[0], viewport_save[1], viewport_save[2], viewport_save[3]);
}

static int UI_ParseEvents(SDL_Event *evt)
{
    switch(evt->type)
    {
        case SDL_KEYUP: /* KEYUP & KEYDOWN share same routine */
        case SDL_KEYDOWN:
            {
                int down = evt->type == SDL_KEYDOWN;
                switch(evt->key.keysym.sym)
                {
                    case SDLK_RSHIFT: /* RSHIFT & LSHIFT share same routine */
                    case SDLK_LSHIFT:    nk_input_key(&ctx, NK_KEY_SHIFT, down); break;
                    case SDLK_DELETE:    nk_input_key(&ctx, NK_KEY_DEL, down); break;
                    case SDLK_RETURN:    nk_input_key(&ctx, NK_KEY_ENTER, down); break;
                    case SDLK_TAB:       nk_input_key(&ctx, NK_KEY_TAB, down); break;
                    case SDLK_BACKSPACE: nk_input_key(&ctx, NK_KEY_BACKSPACE, down); break;
                    case SDLK_HOME:      nk_input_key(&ctx, NK_KEY_TEXT_START, down);
                                         nk_input_key(&ctx, NK_KEY_SCROLL_START, down); break;
                    case SDLK_END:       nk_input_key(&ctx, NK_KEY_TEXT_END, down);
                                         nk_input_key(&ctx, NK_KEY_SCROLL_END, down); break;
                    case SDLK_PAGEDOWN:  nk_input_key(&ctx, NK_KEY_SCROLL_DOWN, down); break;
                    case SDLK_PAGEUP:    nk_input_key(&ctx, NK_KEY_SCROLL_UP, down); break;
                    case SDLK_z:         nk_input_key(&ctx, NK_KEY_TEXT_UNDO, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_r:         nk_input_key(&ctx, NK_KEY_TEXT_REDO, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_c:         nk_input_key(&ctx, NK_KEY_COPY, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_v:         nk_input_key(&ctx, NK_KEY_PASTE, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_x:         nk_input_key(&ctx, NK_KEY_CUT, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_b:         nk_input_key(&ctx, NK_KEY_TEXT_LINE_START, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_e:         nk_input_key(&ctx, NK_KEY_TEXT_LINE_END, down && keyset[SDL_SCANCODE_LCTRL]); break;
                    case SDLK_UP:        nk_input_key(&ctx, NK_KEY_UP, down); break;
                    case SDLK_DOWN:      nk_input_key(&ctx, NK_KEY_DOWN, down); break;
                    case SDLK_LEFT:
                        if (keyset[SDL_SCANCODE_LCTRL])
                            nk_input_key(&ctx, NK_KEY_TEXT_WORD_LEFT, down);
                        else nk_input_key(&ctx, NK_KEY_LEFT, down);
                        break;
                    case SDLK_RIGHT:
                        if (keyset[SDL_SCANCODE_LCTRL])
                            nk_input_key(&ctx, NK_KEY_TEXT_WORD_RIGHT, down);
                        else nk_input_key(&ctx, NK_KEY_RIGHT, down);
                        break;
                }
            }
            return 1;

        case SDL_MOUSEBUTTONUP: /* MOUSEBUTTONUP & MOUSEBUTTONDOWN share same routine */
        case SDL_MOUSEBUTTONDOWN:
            {
                int down = evt->type == SDL_MOUSEBUTTONDOWN;
                const int x = evt->button.x, y = evt->button.y;
                switch(evt->button.button)
                {
                    case SDL_BUTTON_LEFT:
                        if (evt->button.clicks > 1)
                            nk_input_button(&ctx, NK_BUTTON_DOUBLE, x, y, down);
                        nk_input_button(&ctx, NK_BUTTON_LEFT, x, y, down); break;
                    case SDL_BUTTON_MIDDLE: nk_input_button(&ctx, NK_BUTTON_MIDDLE, x, y, down); break;
                    case SDL_BUTTON_RIGHT:  nk_input_button(&ctx, NK_BUTTON_RIGHT, x, y, down); break;
                }
            }
            return 1;

        case SDL_MOUSEMOTION:
            if (ctx.input.mouse.grabbed) {
                int x = (int)ctx.input.mouse.prev.x, y = (int)ctx.input.mouse.prev.y;
                nk_input_motion(&ctx, x + evt->motion.xrel, y + evt->motion.yrel);
            }
            else nk_input_motion(&ctx, evt->motion.x, evt->motion.y);
            return 1;

        case SDL_TEXTINPUT:
            {
                nk_glyph glyph;
                memcpy(glyph, evt->text.text, NK_UTF_SIZE);
                nk_input_glyph(&ctx, glyph);
            }
            return 1;

        case SDL_MOUSEWHEEL:
            nk_input_scroll(&ctx,nk_vec2((float)evt->wheel.x,(float)evt->wheel.y));
            return 1;
    }
    return 0;
}

int UI_HandleEvents(SDL_Event *evt)
{
    nk_input_begin(&ctx);
    int result = UI_ParseEvents(evt);
    nk_input_end(&ctx);

    return result;
}
StrikerMan780 commented 6 months ago

I need some help with this one if anyone can. I've been at this one for a few days now and I'm losing my mind. Video in first post demonstrating what's going on.

StrikerMan780 commented 2 months ago

image image 2

Help? Please? This problem is still happening with the latest version, and I've been struggling with it since getting the library back in May. I'm running out of ideas, and time. Going to have to abandon Nuklear before long if there's no solution.

StrikerMan780 commented 2 months ago

I'm noticing the biggest culprits are buttons and sliders. If I just use labels, it's a tiny bit less prone to this problem.

StrikerMan780 commented 2 months ago

And upon even further inspection, it seems to be related more specifically to NK_TEXT_CENTERED. There seems to be some kind of bug with how it calculates the width of the element. Because if I use centered text with labels, it behaves exactly like buttons do. (Aka. Borked)

StrikerMan780 commented 2 months ago

If I comment out line 23660 in nuklear.h, in nk_widget_text. The line that reads: if (label.w >= label.x) label.w -= label.x;

It seemingly fixes the bug. Consider giving this a look over.

fkallevik commented 3 weeks ago

@StrikerMan780 I am having this exact issue in my project as well. But it doesn't happen in the allegro5 demo I tested, so that indicates to me that I am doing something wrong, but idk..

This exhibits the strange text clipping

https://github.com/user-attachments/assets/83255a41-a3d1-4b77-92ed-6e19a0f13bfa

This works as expected

https://github.com/user-attachments/assets/e7d6c091-f756-42b7-a94c-54fa96ba2d71

fkallevik commented 3 weeks ago

I managed to fix this by correcting the nk.height value I set in my_font_create_from_file and the width returned from my_font_get_text_width. In my case I was scaling them to handle high DPI, so I just changed them to return unscaled values, and then I only apply scaling factor when rendering the text in NK_COMMAND_TEXT.

No strange clipping anymore🎉 https://github.com/user-attachments/assets/e59acbc5-a66d-4b37-bf10-16a06cb3fbb1

StrikerMan780 commented 3 weeks ago

Still can't find a way to fix it on my end without altering that line in Nuklear. The values I return from my text width functions are correct, it's the width of the text in pixels.